From ada98be3d7d212c1327f00bf3fd2ed1832a4483d Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Fri, 7 Nov 2025 15:42:43 +0000 Subject: [PATCH 1/4] Catch DateTimeException in EsqlDataTypeConverter --- .../esql/type/EsqlDataTypeConverter.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 deletions(-) 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 a10094dd93997..87cfe68921c14 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 @@ -73,6 +73,7 @@ import java.io.IOException; import java.math.BigInteger; +import java.time.DateTimeException; import java.time.Duration; import java.time.Instant; import java.time.Period; @@ -616,20 +617,36 @@ public static BytesRef stringToSpatial(String field) { } public static long dateTimeToLong(String dateTime) { - return DEFAULT_DATE_TIME_FORMATTER.parseMillis(dateTime); + try { + return DEFAULT_DATE_TIME_FORMATTER.parseMillis(dateTime); + } catch (DateTimeException e) { + throw new IllegalArgumentException(e); + } } public static long dateTimeToLong(String dateTime, DateFormatter formatter) { - return formatter == null ? dateTimeToLong(dateTime) : formatter.parseMillis(dateTime); + try { + return formatter == null ? dateTimeToLong(dateTime) : formatter.parseMillis(dateTime); + } catch (DateTimeException e) { + throw new IllegalArgumentException(e); + } } public static long dateNanosToLong(String dateNano) { - return dateNanosToLong(dateNano, DEFAULT_DATE_NANOS_FORMATTER); + try { + return dateNanosToLong(dateNano, DEFAULT_DATE_NANOS_FORMATTER); + } catch (DateTimeException e) { + throw new IllegalArgumentException(e); + } } public static long dateNanosToLong(String dateNano, DateFormatter formatter) { - Instant parsed = DateFormatters.from(formatter.parse(dateNano)).toInstant(); - return DateUtils.toLong(parsed); + try { + Instant parsed = DateFormatters.from(formatter.parse(dateNano)).toInstant(); + return DateUtils.toLong(parsed); + } catch (DateTimeException e) { + throw new IllegalArgumentException(e); + } } public static String dateWithTypeToString(long dateTime, DataType type) { From ade207b55c8e7820496d934c2b7a4f2809c0ac96 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Fri, 7 Nov 2025 15:45:24 +0000 Subject: [PATCH 2/4] Update docs/changelog/137744.yaml --- docs/changelog/137744.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/changelog/137744.yaml diff --git a/docs/changelog/137744.yaml b/docs/changelog/137744.yaml new file mode 100644 index 0000000000000..07785a1247004 --- /dev/null +++ b/docs/changelog/137744.yaml @@ -0,0 +1,6 @@ +pr: 137744 +summary: Catch `DateTimeException` in `EsqlDataTypeConverter` +area: ES|QL +type: bug +issues: + - 137741 From 0aa64c90f26b06b24d44acae2013ee01f2f263fb Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Thu, 13 Nov 2025 16:16:15 +0000 Subject: [PATCH 3/4] Add some tests --- .../src/main/resources/date.csv-spec | 26 +++++++++++++++++++ .../function/scalar/convert/ToDateNanos.java | 10 +++++-- .../esql/type/EsqlDataTypeConverter.java | 8 +++--- .../function/scalar/date/DateParseTests.java | 18 +++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec index c3bf0d9d0ebbf..87c7c1a6d8804 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec @@ -515,6 +515,14 @@ b:datetime null ; +evalDateParseInvalidDate +row a = "2026-02-29 foo" | eval b = date_parse("yyyy-MM-dd", a) | keep b; +warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded. +warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'February 29' as '2026' is not a leap year +b:datetime +null +; + evalDateParseNotMatching row a = "2023-02-01" | eval b = date_parse("yyyy-MM", a) | keep b; warning:Line 1:33: evaluation of [date_parse(\"yyyy-MM\", a)] failed, treating result as null. Only first 20 failures recorded. @@ -1611,6 +1619,24 @@ result:boolean null ; +evalInvalidDate +ROW x = ["2026-04-31"]::DATE | KEEP x; +warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded. +warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'February 29' as '2026' is not a leap year +; + +evalInvalidDateTime +ROW x = ["2026-04-31"]::DATE_NANOS | KEEP x; +warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded. +warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'February 29' as '2026' is not a leap year +; + +evalInvalidDateTime +ROW x = ["2026-04-31"]::DATETIME | KEEP x; +warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded. +warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'February 29' as '2026' is not a leap year +; + evalDateTruncYearInArbitraryIntervals required_capability: date_trunc_with_arbitrary_intervals diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDateNanos.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDateNanos.java index 75ec0d68c1a8b..0a306a7cd13e8 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDateNanos.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/convert/ToDateNanos.java @@ -24,6 +24,7 @@ import org.elasticsearch.xpack.esql.expression.function.Param; import java.io.IOException; +import java.time.DateTimeException; import java.time.Instant; import java.util.List; import java.util.Map; @@ -125,8 +126,13 @@ static long fromDouble(double in) { @ConvertEvaluator(extraName = "FromString", warnExceptions = { IllegalArgumentException.class }) static long fromKeyword(BytesRef in) { - Instant parsed = DateFormatters.from(DEFAULT_DATE_NANOS_FORMATTER.parse(in.utf8ToString())).toInstant(); - return DateUtils.toLong(parsed); + try { + Instant parsed = DateFormatters.from(DEFAULT_DATE_NANOS_FORMATTER.parse(in.utf8ToString())).toInstant(); + return DateUtils.toLong(parsed); + } catch (DateTimeException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } @ConvertEvaluator(extraName = "FromDatetime", warnExceptions = { IllegalArgumentException.class }) 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 87cfe68921c14..129a2fb91801c 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 @@ -620,7 +620,7 @@ public static long dateTimeToLong(String dateTime) { try { return DEFAULT_DATE_TIME_FORMATTER.parseMillis(dateTime); } catch (DateTimeException e) { - throw new IllegalArgumentException(e); + throw new IllegalArgumentException(e.getMessage(), e); } } @@ -628,7 +628,7 @@ public static long dateTimeToLong(String dateTime, DateFormatter formatter) { try { return formatter == null ? dateTimeToLong(dateTime) : formatter.parseMillis(dateTime); } catch (DateTimeException e) { - throw new IllegalArgumentException(e); + throw new IllegalArgumentException(e.getMessage(), e); } } @@ -636,7 +636,7 @@ public static long dateNanosToLong(String dateNano) { try { return dateNanosToLong(dateNano, DEFAULT_DATE_NANOS_FORMATTER); } catch (DateTimeException e) { - throw new IllegalArgumentException(e); + throw new IllegalArgumentException(e.getMessage(), e); } } @@ -645,7 +645,7 @@ public static long dateNanosToLong(String dateNano, DateFormatter formatter) { Instant parsed = DateFormatters.from(formatter.parse(dateNano)).toInstant(); return DateUtils.toLong(parsed); } catch (DateTimeException e) { - throw new IllegalArgumentException(e); + throw new IllegalArgumentException(e.getMessage(), e); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseTests.java index 6aded21e11a98..85ac3b9ad0982 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseTests.java @@ -153,6 +153,24 @@ public static Iterable parameters() { ) ) ); + cases.add( + new TestCaseSupplier( + List.of(DataType.KEYWORD, DataType.KEYWORD), + () -> new TestCaseSupplier.TestCase( + List.of( + new TestCaseSupplier.TypedData(new BytesRef("yyyy-MM-dd"), DataType.KEYWORD, "first"), + new TestCaseSupplier.TypedData(new BytesRef("2026-02-29"), DataType.KEYWORD, "second") + + ), + "DateParseEvaluator[val=Attribute[channel=1], formatter=Attribute[channel=0]]", + DataType.DATETIME, + is(nullValue()) + ).withWarning("Line 1:1: evaluation of [source] failed, treating result as null. Only first 20 failures recorded.") + .withWarning( + "Line 1:1: java.lang.IllegalArgumentException: " + "Invalid date 'February 29' as '2026' is not a leap year" + ) + ) + ); cases = anyNullIsNull(true, cases); cases.add( new TestCaseSupplier( From 3f40a5fd57de45e04e0a973e6b9b8c96d8da24d8 Mon Sep 17 00:00:00 2001 From: Simon Cooper Date: Thu, 13 Nov 2025 16:50:48 +0000 Subject: [PATCH 4/4] Add a capability --- .../qa/testFixtures/src/main/resources/date.csv-spec | 12 ++++++++---- .../xpack/esql/action/EsqlCapabilities.java | 2 ++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec index 87c7c1a6d8804..19200c12ff9e2 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/date.csv-spec @@ -516,6 +516,7 @@ null ; evalDateParseInvalidDate +required_capability: date_time_exceptions_handled row a = "2026-02-29 foo" | eval b = date_parse("yyyy-MM-dd", a) | keep b; warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded. warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'February 29' as '2026' is not a leap year @@ -1620,21 +1621,24 @@ null ; evalInvalidDate +required_capability: date_time_exceptions_handled ROW x = ["2026-04-31"]::DATE | KEEP x; warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded. -warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'February 29' as '2026' is not a leap year +warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'APRIL 31' ; -evalInvalidDateTime +evalInvalidDateNanos +required_capability: date_time_exceptions_handled ROW x = ["2026-04-31"]::DATE_NANOS | KEEP x; warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded. -warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'February 29' as '2026' is not a leap year +warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'APRIL 31' ; evalInvalidDateTime +required_capability: date_time_exceptions_handled ROW x = ["2026-04-31"]::DATETIME | KEEP x; warning:Line 1:37: evaluation of [date_parse(\"yyyy-MM-dd\", a)] failed, treating result as null. Only first 20 failures recorded. -warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'February 29' as '2026' is not a leap year +warning:Line 1:37: java.lang.IllegalArgumentException: Invalid date 'APRIL 31' ; evalDateTruncYearInArbitraryIntervals diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java index 68081ea2872e1..5abb36b4c3e1b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java @@ -1647,6 +1647,8 @@ public enum Cap { */ EXPONENTIAL_HISTOGRAM_PERCENTILES_SUPPORT(EXPONENTIAL_HISTOGRAM_FEATURE_FLAG), + DATE_TIME_EXCEPTIONS_HANDLED, + // Last capability should still have a comma for fewer merge conflicts when adding new ones :) // This comment prevents the semicolon from being on the previous capability when Spotless formats the file. ;