From acc9837792de5bb26b42b911b234f7eb309ccbfe Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 14 Oct 2025 18:09:54 +0200 Subject: [PATCH 1/9] date parse settings --- .../function/EsqlFunctionRegistry.java | 6 +- .../function/ThreeOptionalArguments.java | 16 +++++ .../function/scalar/date/DateParse.java | 71 +++++++++++++++++-- 3 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ThreeOptionalArguments.java diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index c6520c8563d6d..41f1b25ffd8be 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -1065,13 +1065,17 @@ protected static FunctionDefinition def(Class function, if (children.size() > 4 || children.size() < 2) { throw new QlIllegalArgumentException("expects minimum two, maximum four arguments"); } + } else if (ThreeOptionalArguments.class.isAssignableFrom(function)) { + if (children.size() > 4 || children.isEmpty()) { + throw new QlIllegalArgumentException("expects minimum one, maximum four arguments"); + } } else if (children.size() != 4) { throw new QlIllegalArgumentException("expects exactly four arguments"); } return ctorRef.build( source, children.get(0), - children.get(1), + children.size() > 1 ? children.get(1) : null, children.size() > 2 ? children.get(2) : null, children.size() > 3 ? children.get(3) : null ); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ThreeOptionalArguments.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ThreeOptionalArguments.java new file mode 100644 index 0000000000000..be464c05ef6cb --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ThreeOptionalArguments.java @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.expression.function; + +/** + * Marker interface indicating that a function accepts three optional arguments (the last three). + * This is used by the {@link EsqlFunctionRegistry} to perform validation of function declaration. + */ +public interface ThreeOptionalArguments { + +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java index ef1acbc395308..d3d6b77e0214b 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java @@ -24,11 +24,18 @@ import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; import org.elasticsearch.xpack.esql.expression.function.OptionalArgument; import org.elasticsearch.xpack.esql.expression.function.Param; +import org.elasticsearch.xpack.esql.expression.function.ThreeOptionalArguments; +import org.elasticsearch.xpack.esql.expression.function.TwoOptionalArguments; import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.TimeZone; import static org.elasticsearch.common.time.DateFormatter.forPattern; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; @@ -38,7 +45,7 @@ import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong; -public class DateParse extends EsqlScalarFunction implements OptionalArgument { +public class DateParse extends EsqlScalarFunction implements ThreeOptionalArguments { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( Expression.class, "DateParse", @@ -47,6 +54,8 @@ public class DateParse extends EsqlScalarFunction implements OptionalArgument { private final Expression field; private final Expression format; + private final Expression locale; + private final Expression timezone; @FunctionInfo( returnType = "date", @@ -63,17 +72,46 @@ public DateParse( name = "dateString", type = { "keyword", "text" }, description = "Date expression as a string. If `null` or an empty string, the function returns `null`." - ) Expression second + ) Expression second, + @Param( + name="dateLocale", + type = { "keyword", "text" }, + description = "The locale to parse with" + ) Expression third, + @Param( + name="dateTimezone", + type = { "keyword", "text" }, + description = "The timezone to parse with" + ) Expression forth ) { - super(source, second != null ? List.of(first, second) : List.of(first)); + super(source, fields(first, second, third, forth)); this.field = second != null ? second : first; this.format = second != null ? first : null; + this.locale = third; + this.timezone = forth; + } + + private static List fields(Expression field, Expression format, Expression locale, Expression timezone) { + List list = new ArrayList<>(3); + list.add(field); + if (format != null) { + list.add(format); + } + if (locale != null) { + list.add(locale); + } + if (timezone != null) { + list.add(timezone); + } + return list; } private DateParse(StreamInput in) throws IOException { this( Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), + in.readOptionalNamedWriteable(Expression.class), + in.readOptionalNamedWriteable(Expression.class), in.readOptionalNamedWriteable(Expression.class) ); } @@ -82,7 +120,9 @@ private DateParse(StreamInput in) throws IOException { public void writeTo(StreamOutput out) throws IOException { source().writeTo(out); out.writeNamedWriteable(children().get(0)); - out.writeOptionalNamedWriteable(children().size() == 2 ? children().get(1) : null); + out.writeOptionalNamedWriteable(children().size() > 1 ? children().get(1) : null); + out.writeOptionalNamedWriteable(children().size() > 2 ? children().get(2) : null); + out.writeOptionalNamedWriteable(children().size() > 3 ? children().get(3) : null); } @Override @@ -141,9 +181,23 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { if (DataType.isString(format.dataType()) == false) { throw new IllegalArgumentException("unsupported data type for date_parse [" + format.dataType() + "]"); } + String localeAsString = locale == null ? null : ((BytesRef) locale.fold(toEvaluator.foldCtx())).utf8ToString(); + Locale locale = localeAsString == null ? null : Locale.forLanguageTag(localeAsString); + if (localeAsString != null && locale == null) { + throw new IllegalArgumentException("unsupported locale [" + localeAsString + "]"); + } + + String timezoneAsString = timezone == null ? null : ((BytesRef) timezone.fold(toEvaluator.foldCtx())).utf8ToString(); + TimeZone timezone = timezoneAsString == null ? null : TimeZone.getTimeZone(timezoneAsString); if (format.foldable()) { try { DateFormatter formatter = toFormatter(format.fold(toEvaluator.foldCtx())); + if (locale != null) { + formatter = formatter.withLocale(locale); + } + if (timezone != null) { + formatter = formatter.withZone(timezone.toZoneId()); + } return new DateParseConstantEvaluator.Factory(source(), fieldEvaluator, formatter); } catch (IllegalArgumentException e) { throw new InvalidArgumentException(e, "invalid date pattern for [{}]: {}", sourceText(), e.getMessage()); @@ -159,13 +213,18 @@ private static DateFormatter toFormatter(Object format) { @Override public Expression replaceChildren(List newChildren) { - return new DateParse(source(), newChildren.get(0), newChildren.size() > 1 ? newChildren.get(1) : null); + return new DateParse( + source(), + newChildren.get(0), + newChildren.size() > 1 ? newChildren.get(1) : null, + newChildren.size() > 2 ? newChildren.get(2) : null, + newChildren.size() > 3 ? newChildren.get(3) : null); } @Override protected NodeInfo info() { Expression first = format != null ? format : field; Expression second = format != null ? field : null; - return NodeInfo.create(this, DateParse::new, first, second); + return NodeInfo.create(this, DateParse::new, first, second, locale, timezone); } } From e9ee7b820281e57a37205c07ac17d7227fd5e357 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 14 Oct 2025 16:21:32 +0000 Subject: [PATCH 2/9] [CI] Auto commit changes from spotless --- .../function/scalar/date/DateParse.java | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java index d3d6b77e0214b..bc24cc7f9b41c 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java @@ -22,19 +22,15 @@ import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; -import org.elasticsearch.xpack.esql.expression.function.OptionalArgument; import org.elasticsearch.xpack.esql.expression.function.Param; import org.elasticsearch.xpack.esql.expression.function.ThreeOptionalArguments; -import org.elasticsearch.xpack.esql.expression.function.TwoOptionalArguments; import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.TimeZone; import static org.elasticsearch.common.time.DateFormatter.forPattern; @@ -73,16 +69,8 @@ public DateParse( type = { "keyword", "text" }, description = "Date expression as a string. If `null` or an empty string, the function returns `null`." ) Expression second, - @Param( - name="dateLocale", - type = { "keyword", "text" }, - description = "The locale to parse with" - ) Expression third, - @Param( - name="dateTimezone", - type = { "keyword", "text" }, - description = "The timezone to parse with" - ) Expression forth + @Param(name = "dateLocale", type = { "keyword", "text" }, description = "The locale to parse with") Expression third, + @Param(name = "dateTimezone", type = { "keyword", "text" }, description = "The timezone to parse with") Expression forth ) { super(source, fields(first, second, third, forth)); this.field = second != null ? second : first; @@ -218,7 +206,8 @@ public Expression replaceChildren(List newChildren) { newChildren.get(0), newChildren.size() > 1 ? newChildren.get(1) : null, newChildren.size() > 2 ? newChildren.get(2) : null, - newChildren.size() > 3 ? newChildren.get(3) : null); + newChildren.size() > 3 ? newChildren.get(3) : null + ); } @Override From 0f392752f97c2a9158211d85344123b45333799e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 16 Oct 2025 18:32:25 +0200 Subject: [PATCH 3/9] work work work --- .../src/main/resources/date.csv-spec | 21 ++++ .../function/EsqlFunctionRegistry.java | 11 +-- .../function/ThreeOptionalArguments.java | 16 --- .../function/scalar/date/DateParse.java | 98 ++++++++++++++----- .../scalar/date/DateParseErrorTests.java | 2 +- .../date/DateParseSerializationTests.java | 10 +- .../function/scalar/date/DateParseTests.java | 48 ++++++++- .../rules/logical/FoldNullTests.java | 2 +- 8 files changed, 153 insertions(+), 55 deletions(-) delete mode 100644 x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ThreeOptionalArguments.java 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 e5efc9e7a37aa..d53440d341b0a 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 @@ -644,6 +644,27 @@ emp_no:integer | new_date:datetime | birth_date:datetime | bool: 10050 | 1958-05-21T00:00:00.000Z | 1958-05-21T00:00:00.000Z | true ; +evalDateParseWithTimezone +row a = "10-10-25" | eval b = date_parse(a, "dd-mm-yyyy", {"time_zone":"Europe/Paris"}) | keep b; + +b:datetime +2024-12-31T23:00:00.000Z +; + +evalDateParseWithLocale +row a = "10 septembre 2025" | eval b = date_parse(a, "dd-mm-yyyy", {"locale":"fr"}) | keep b; + +b:datetime +2025-09-10T00:00:00.000Z +; + +evalDateParseWithLocaleAndTimezone +row a = "10 septembre 2025" | eval b = date_parse(a, "dd-mm-yyyy", {"locale":"fr","time_zone":"Europe/Paris"}) | keep b; + +b:datetime +2025-09-09T22:00:00.000Z +; + dateFields from employees | where emp_no == 10049 or emp_no == 10050 | eval year = date_extract("year", birth_date), month = date_extract("month_of_year", birth_date), day = date_extract("day_of_month", birth_date) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index 41f1b25ffd8be..b1a7f79bd712f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -1037,7 +1037,10 @@ public interface BinaryBuilder { protected static FunctionDefinition def(Class function, TernaryBuilder ctorRef, String... names) { FunctionBuilder builder = (source, children, cfg) -> { boolean hasMinimumTwo = OptionalArgument.class.isAssignableFrom(function); - if (hasMinimumTwo && (children.size() > 3 || children.size() < 2)) { + boolean hasMinimumOne = TwoOptionalArguments.class.isAssignableFrom(function); + if (hasMinimumOne && (children.size() > 3 || children.isEmpty())) { + throw new QlIllegalArgumentException("expects one, two or three arguments"); + } else if (hasMinimumTwo && (children.size() > 3 || children.size() < 2)) { throw new QlIllegalArgumentException("expects two or three arguments"); } else if (hasMinimumTwo == false && children.size() != 3) { throw new QlIllegalArgumentException("expects exactly three arguments"); @@ -1065,17 +1068,13 @@ protected static FunctionDefinition def(Class function, if (children.size() > 4 || children.size() < 2) { throw new QlIllegalArgumentException("expects minimum two, maximum four arguments"); } - } else if (ThreeOptionalArguments.class.isAssignableFrom(function)) { - if (children.size() > 4 || children.isEmpty()) { - throw new QlIllegalArgumentException("expects minimum one, maximum four arguments"); - } } else if (children.size() != 4) { throw new QlIllegalArgumentException("expects exactly four arguments"); } return ctorRef.build( source, children.get(0), - children.size() > 1 ? children.get(1) : null, + children.get(1), children.size() > 2 ? children.get(2) : null, children.size() > 3 ? children.get(3) : null ); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ThreeOptionalArguments.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ThreeOptionalArguments.java deleted file mode 100644 index be464c05ef6cb..0000000000000 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/ThreeOptionalArguments.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -package org.elasticsearch.xpack.esql.expression.function; - -/** - * Marker interface indicating that a function accepts three optional arguments (the last three). - * This is used by the {@link EsqlFunctionRegistry} to perform validation of function declaration. - */ -public interface ThreeOptionalArguments { - -} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java index bc24cc7f9b41c..cdba6688800f3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java @@ -12,46 +12,58 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.time.DateFormatter; +import org.elasticsearch.common.util.LocaleUtils; import org.elasticsearch.compute.ann.Evaluator; import org.elasticsearch.compute.ann.Fixed; import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; +import org.elasticsearch.xpack.esql.core.expression.MapExpression; import org.elasticsearch.xpack.esql.core.tree.NodeInfo; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.Example; import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; +import org.elasticsearch.xpack.esql.expression.function.MapParam; +import org.elasticsearch.xpack.esql.expression.function.Options; import org.elasticsearch.xpack.esql.expression.function.Param; -import org.elasticsearch.xpack.esql.expression.function.ThreeOptionalArguments; +import org.elasticsearch.xpack.esql.expression.function.TwoOptionalArguments; import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction; import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import java.io.IOException; +import java.time.ZoneId; +import java.time.zone.ZoneRulesException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.TimeZone; +import java.util.Map; +import static java.util.Map.entry; import static org.elasticsearch.common.time.DateFormatter.forPattern; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; +import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString; +import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD; import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isStringAndExact; import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER; import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong; -public class DateParse extends EsqlScalarFunction implements ThreeOptionalArguments { +public class DateParse extends EsqlScalarFunction implements TwoOptionalArguments { public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry( Expression.class, "DateParse", DateParse::new ); + private static final String TIME_ZONE_PARAM_NAME = "time_zone"; + private static final String LOCALE_PARAM_NAME = "locale"; + private final Expression field; private final Expression format; - private final Expression locale; - private final Expression timezone; + private final Expression options; @FunctionInfo( returnType = "date", @@ -69,27 +81,40 @@ public DateParse( type = { "keyword", "text" }, description = "Date expression as a string. If `null` or an empty string, the function returns `null`." ) Expression second, - @Param(name = "dateLocale", type = { "keyword", "text" }, description = "The locale to parse with") Expression third, - @Param(name = "dateTimezone", type = { "keyword", "text" }, description = "The timezone to parse with") Expression forth + @MapParam( + name = "options", + params = { + @MapParam.MapParamEntry( + name = TIME_ZONE_PARAM_NAME, + type = "keyword", + valueHint = { "standard" }, + description = "Coordinated Universal Time (UTC) offset or IANA time zone used to convert date values in the " + + "query string to UTC." + ), + @MapParam.MapParamEntry( + name = LOCALE_PARAM_NAME, + type = "keyword", + valueHint = { "standard" }, + description = "The locale to use when parsing the date, relevant when parsing month names or week days." + ) + }, + description = "(Optional) Additional options for date parsing as <>.", + optional = true) Expression options ) { - super(source, fields(first, second, third, forth)); + super(source, fields(first, second, options)); this.field = second != null ? second : first; this.format = second != null ? first : null; - this.locale = third; - this.timezone = forth; + this.options = options; } - private static List fields(Expression field, Expression format, Expression locale, Expression timezone) { + private static List fields(Expression field, Expression format, Expression options) { List list = new ArrayList<>(3); list.add(field); if (format != null) { list.add(format); } - if (locale != null) { - list.add(locale); - } - if (timezone != null) { - list.add(timezone); + if (options != null) { + list.add(options); } return list; } @@ -99,7 +124,6 @@ private DateParse(StreamInput in) throws IOException { Source.readFrom((PlanStreamInput) in), in.readNamedWriteable(Expression.class), in.readOptionalNamedWriteable(Expression.class), - in.readOptionalNamedWriteable(Expression.class), in.readOptionalNamedWriteable(Expression.class) ); } @@ -160,6 +184,21 @@ static long process(BytesRef val, BytesRef formatter) throws IllegalArgumentExce return dateTimeToLong(val.utf8ToString(), toFormatter(formatter)); } + public static final Map ALLOWED_OPTIONS = Map.ofEntries( + entry(TIME_ZONE_PARAM_NAME, KEYWORD), + entry(LOCALE_PARAM_NAME, KEYWORD) + ); + + private Map parseOptions() throws InvalidArgumentException { + Map matchOptions = new HashMap<>(); + if (this.options == null) { + return matchOptions; + } + + Options.populateMap((MapExpression) this.options, matchOptions, source(), THIRD, ALLOWED_OPTIONS); + return matchOptions; + } + @Override public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { ExpressionEvaluator.Factory fieldEvaluator = toEvaluator.apply(field); @@ -169,14 +208,20 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { if (DataType.isString(format.dataType()) == false) { throw new IllegalArgumentException("unsupported data type for date_parse [" + format.dataType() + "]"); } - String localeAsString = locale == null ? null : ((BytesRef) locale.fold(toEvaluator.foldCtx())).utf8ToString(); - Locale locale = localeAsString == null ? null : Locale.forLanguageTag(localeAsString); - if (localeAsString != null && locale == null) { - throw new IllegalArgumentException("unsupported locale [" + localeAsString + "]"); + var parsedOptions = this.parseOptions(); + String localeAsString = (String)parsedOptions.get(LOCALE_PARAM_NAME); + Locale locale = localeAsString == null ? null : LocaleUtils.parse(localeAsString); + + String timezoneAsString = (String)parsedOptions.get(TIME_ZONE_PARAM_NAME); + ZoneId timezone = null; + try { + if (timezoneAsString != null) { + timezone = ZoneId.of(timezoneAsString); + } + } catch (ZoneRulesException e) { + throw new IllegalArgumentException("unsupported timezone [" + timezoneAsString + "]"); } - String timezoneAsString = timezone == null ? null : ((BytesRef) timezone.fold(toEvaluator.foldCtx())).utf8ToString(); - TimeZone timezone = timezoneAsString == null ? null : TimeZone.getTimeZone(timezoneAsString); if (format.foldable()) { try { DateFormatter formatter = toFormatter(format.fold(toEvaluator.foldCtx())); @@ -184,7 +229,7 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { formatter = formatter.withLocale(locale); } if (timezone != null) { - formatter = formatter.withZone(timezone.toZoneId()); + formatter = formatter.withZone(timezone); } return new DateParseConstantEvaluator.Factory(source(), fieldEvaluator, formatter); } catch (IllegalArgumentException e) { @@ -205,8 +250,7 @@ public Expression replaceChildren(List newChildren) { source(), newChildren.get(0), newChildren.size() > 1 ? newChildren.get(1) : null, - newChildren.size() > 2 ? newChildren.get(2) : null, - newChildren.size() > 3 ? newChildren.get(3) : null + newChildren.size() > 2 ? newChildren.get(2) : null ); } @@ -214,6 +258,6 @@ public Expression replaceChildren(List newChildren) { protected NodeInfo info() { Expression first = format != null ? format : field; Expression second = format != null ? field : null; - return NodeInfo.create(this, DateParse::new, first, second, locale, timezone); + return NodeInfo.create(this, DateParse::new, first, second, options); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseErrorTests.java index 21d9b5fb00537..75b47934b6a04 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseErrorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseErrorTests.java @@ -27,7 +27,7 @@ protected List cases() { @Override protected Expression build(Source source, List args) { - return new DateParse(source, args.get(0), args.size() > 1 ? args.get(1) : null); + return new DateParse(source, args.get(0), args.size() > 1 ? args.get(1) : null, args.size() == 3 ? args.get(2) : null); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseSerializationTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseSerializationTests.java index 79a650c8dd963..dcf47aa82f94d 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseSerializationTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParseSerializationTests.java @@ -19,7 +19,8 @@ protected DateParse createTestInstance() { Source source = randomSource(); Expression first = randomChild(); Expression second = randomBoolean() ? null : randomChild(); - return new DateParse(source, first, second); + Expression options = second != null && randomBoolean() ? randomChild() : null; + return new DateParse(source, first, second, options); } @Override @@ -32,6 +33,11 @@ protected DateParse mutateInstance(DateParse instance) throws IOException { } else { second = randomValueOtherThan(second, () -> randomBoolean() ? null : randomChild()); } - return new DateParse(source, first, second); + Expression options = instance.children().size() == 3 ? instance.children().get(2) : null; + + if (randomBoolean()) { + options = randomValueOtherThan(first, AbstractExpressionSerializationTests::randomChild); + } + return new DateParse(source, first, second, options); } } 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 8b53a1b9112b4..eef0fc16993b2 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 @@ -15,6 +15,7 @@ import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.Literal; +import org.elasticsearch.xpack.esql.core.expression.MapExpression; import org.elasticsearch.xpack.esql.core.tree.Source; import org.elasticsearch.xpack.esql.core.type.DataType; import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase; @@ -156,15 +157,58 @@ public void testInvalidPattern() { new DateParse( Source.EMPTY, new Literal(Source.EMPTY, new BytesRef(pattern), DataType.KEYWORD), - field("str", DataType.KEYWORD) + field("str", DataType.KEYWORD), + null ) ).get(driverContext) ); assertThat(e.getMessage(), startsWith("invalid date pattern for []: Invalid format: [" + pattern + "]")); } + public void testInvalidLocale() { + String pattern = "YYYY"; + String locale = "NON-EXISTING-LOCALE"; + DriverContext driverContext = driverContext(); + InvalidArgumentException e = expectThrows( + InvalidArgumentException.class, + () -> evaluator( + new DateParse( + Source.EMPTY, + new Literal(Source.EMPTY, new BytesRef(pattern), DataType.KEYWORD), + field("str", DataType.KEYWORD), + new MapExpression(Source.EMPTY, List.of( + new Literal(Source.EMPTY, new BytesRef("locale"), DataType.KEYWORD), + new Literal(Source.EMPTY, new BytesRef(locale), DataType.KEYWORD) + )) + ) + ).get(driverContext) + ); + assertThat(e.getMessage(), startsWith("Unknown language: " + locale)); + } + + public void testInvalidTimezone() { + String pattern = "YYYY"; + String timezone = "NON-EXISTING-TIMEZONE"; + DriverContext driverContext = driverContext(); + InvalidArgumentException e = expectThrows( + InvalidArgumentException.class, + () -> evaluator( + new DateParse( + Source.EMPTY, + new Literal(Source.EMPTY, new BytesRef(pattern), DataType.KEYWORD), + field("str", DataType.KEYWORD), + new MapExpression(Source.EMPTY, List.of( + new Literal(Source.EMPTY, new BytesRef("time_zone"), DataType.KEYWORD), + new Literal(Source.EMPTY, new BytesRef(timezone), DataType.KEYWORD) + )) + ) + ).get(driverContext) + ); + assertThat(e.getMessage(), startsWith("unsupported timezone [" + timezone + "]")); + } + @Override protected Expression build(Source source, List args) { - return new DateParse(source, args.get(0), args.size() > 1 ? args.get(1) : null); + return new DateParse(source, args.get(0), args.size() > 1 ? args.get(1) : null, args.size() == 3 ? args.get(2) : null); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java index ae30dce97ce5a..e45782dcf80ee 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java @@ -97,7 +97,7 @@ public void testBasicNullFolding() { assertNullLiteral(foldNull(new Round(EMPTY, Literal.NULL, null))); assertNullLiteral(foldNull(new Pow(EMPTY, Literal.NULL, Literal.NULL))); assertNullLiteral(foldNull(new DateFormat(EMPTY, Literal.NULL, Literal.NULL, null))); - assertNullLiteral(foldNull(new DateParse(EMPTY, Literal.NULL, Literal.NULL))); + assertNullLiteral(foldNull(new DateParse(EMPTY, Literal.NULL, Literal.NULL, NULL))); assertNullLiteral(foldNull(new DateTrunc(EMPTY, Literal.NULL, Literal.NULL))); assertNullLiteral(foldNull(new Substring(EMPTY, Literal.NULL, Literal.NULL, Literal.NULL))); } From f3932ca45f3b7ecd2f901dafaa6808087cf6452a Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 16 Oct 2025 16:38:49 +0000 Subject: [PATCH 4/9] [CI] Auto commit changes from spotless --- .../function/scalar/date/DateParse.java | 10 ++++----- .../function/scalar/date/DateParseTests.java | 22 ++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java index cdba6688800f3..1f67b9ebec99a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java @@ -96,10 +96,10 @@ public DateParse( type = "keyword", valueHint = { "standard" }, description = "The locale to use when parsing the date, relevant when parsing month names or week days." - ) - }, + ) }, description = "(Optional) Additional options for date parsing as <>.", - optional = true) Expression options + optional = true + ) Expression options ) { super(source, fields(first, second, options)); this.field = second != null ? second : first; @@ -209,10 +209,10 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) { throw new IllegalArgumentException("unsupported data type for date_parse [" + format.dataType() + "]"); } var parsedOptions = this.parseOptions(); - String localeAsString = (String)parsedOptions.get(LOCALE_PARAM_NAME); + String localeAsString = (String) parsedOptions.get(LOCALE_PARAM_NAME); Locale locale = localeAsString == null ? null : LocaleUtils.parse(localeAsString); - String timezoneAsString = (String)parsedOptions.get(TIME_ZONE_PARAM_NAME); + String timezoneAsString = (String) parsedOptions.get(TIME_ZONE_PARAM_NAME); ZoneId timezone = null; try { if (timezoneAsString != null) { 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 eef0fc16993b2..bfa6ec852f763 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 @@ -176,10 +176,13 @@ public void testInvalidLocale() { Source.EMPTY, new Literal(Source.EMPTY, new BytesRef(pattern), DataType.KEYWORD), field("str", DataType.KEYWORD), - new MapExpression(Source.EMPTY, List.of( - new Literal(Source.EMPTY, new BytesRef("locale"), DataType.KEYWORD), - new Literal(Source.EMPTY, new BytesRef(locale), DataType.KEYWORD) - )) + new MapExpression( + Source.EMPTY, + List.of( + new Literal(Source.EMPTY, new BytesRef("locale"), DataType.KEYWORD), + new Literal(Source.EMPTY, new BytesRef(locale), DataType.KEYWORD) + ) + ) ) ).get(driverContext) ); @@ -197,10 +200,13 @@ public void testInvalidTimezone() { Source.EMPTY, new Literal(Source.EMPTY, new BytesRef(pattern), DataType.KEYWORD), field("str", DataType.KEYWORD), - new MapExpression(Source.EMPTY, List.of( - new Literal(Source.EMPTY, new BytesRef("time_zone"), DataType.KEYWORD), - new Literal(Source.EMPTY, new BytesRef(timezone), DataType.KEYWORD) - )) + new MapExpression( + Source.EMPTY, + List.of( + new Literal(Source.EMPTY, new BytesRef("time_zone"), DataType.KEYWORD), + new Literal(Source.EMPTY, new BytesRef(timezone), DataType.KEYWORD) + ) + ) ) ).get(driverContext) ); From 76dedf59a6518320f7a883789c380429ec35dbbb Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 17 Oct 2025 11:43:02 +0200 Subject: [PATCH 5/9] work 2 --- .../functions/functionNamedParams/date_parse.md | 10 ++++++++++ .../esql/_snippets/functions/layout/date_parse.md | 3 +++ .../_snippets/functions/parameters/date_parse.md | 3 +++ .../esql/_snippets/functions/types/date_parse.md | 12 ++++++------ .../esql/images/functions/date_parse.svg | 2 +- .../testFixtures/src/main/resources/date.csv-spec | 15 +++++++++------ .../xpack/esql/action/EsqlCapabilities.java | 5 +++++ .../expression/function/EsqlFunctionRegistry.java | 9 +++++++-- .../function/scalar/date/DateParse.java | 3 +-- .../function/scalar/date/DateParseTests.java | 10 +++++----- 10 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 docs/reference/query-languages/esql/_snippets/functions/functionNamedParams/date_parse.md diff --git a/docs/reference/query-languages/esql/_snippets/functions/functionNamedParams/date_parse.md b/docs/reference/query-languages/esql/_snippets/functions/functionNamedParams/date_parse.md new file mode 100644 index 0000000000000..328494ce830d3 --- /dev/null +++ b/docs/reference/query-languages/esql/_snippets/functions/functionNamedParams/date_parse.md @@ -0,0 +1,10 @@ +% This is generated by ESQL's AbstractFunctionTestCase. Do not edit it. See ../README.md for how to regenerate it. + +**Supported function named parameters** + +`time_zone` +: (keyword) Coordinated Universal Time (UTC) offset or IANA time zone used to convert date values in the query string to UTC. + +`locale` +: (keyword) The locale to use when parsing the date, relevant when parsing month names or week days. + diff --git a/docs/reference/query-languages/esql/_snippets/functions/layout/date_parse.md b/docs/reference/query-languages/esql/_snippets/functions/layout/date_parse.md index 4ac4734ef823b..f9c3d45b01c6d 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/layout/date_parse.md +++ b/docs/reference/query-languages/esql/_snippets/functions/layout/date_parse.md @@ -19,5 +19,8 @@ :::{include} ../types/date_parse.md ::: +:::{include} ../functionNamedParams/date_parse.md +::: + :::{include} ../examples/date_parse.md ::: diff --git a/docs/reference/query-languages/esql/_snippets/functions/parameters/date_parse.md b/docs/reference/query-languages/esql/_snippets/functions/parameters/date_parse.md index adc0a0c86b19d..b10bec180329c 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/parameters/date_parse.md +++ b/docs/reference/query-languages/esql/_snippets/functions/parameters/date_parse.md @@ -8,3 +8,6 @@ `dateString` : Date expression as a string. If `null` or an empty string, the function returns `null`. +`options` +: (Optional) Additional options for date parsing, specifying time zone and locale as [function named parameters](/reference/query-languages/esql/esql-syntax.md#esql-function-named-params). + diff --git a/docs/reference/query-languages/esql/_snippets/functions/types/date_parse.md b/docs/reference/query-languages/esql/_snippets/functions/types/date_parse.md index 4f1873cd3796b..4dec105ca46d4 100644 --- a/docs/reference/query-languages/esql/_snippets/functions/types/date_parse.md +++ b/docs/reference/query-languages/esql/_snippets/functions/types/date_parse.md @@ -2,10 +2,10 @@ **Supported types** -| datePattern | dateString | result | -| --- | --- | --- | -| keyword | keyword | date | -| keyword | text | date | -| text | keyword | date | -| text | text | date | +| datePattern | dateString | options | result | +| --- | --- | --- | --- | +| keyword | keyword | | date | +| keyword | text | | date | +| text | keyword | | date | +| text | text | | date | diff --git a/docs/reference/query-languages/esql/images/functions/date_parse.svg b/docs/reference/query-languages/esql/images/functions/date_parse.svg index 0f5e5f624143a..4a16582db66f8 100644 --- a/docs/reference/query-languages/esql/images/functions/date_parse.svg +++ b/docs/reference/query-languages/esql/images/functions/date_parse.svg @@ -1 +1 @@ -DATE_PARSE(datePattern,dateString) \ No newline at end of file +DATE_PARSE(datePattern,dateString,options) \ No newline at end of file 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 d53440d341b0a..75eee9ba80c00 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 @@ -644,22 +644,25 @@ emp_no:integer | new_date:datetime | birth_date:datetime | bool: 10050 | 1958-05-21T00:00:00.000Z | 1958-05-21T00:00:00.000Z | true ; -evalDateParseWithTimezone -row a = "10-10-25" | eval b = date_parse(a, "dd-mm-yyyy", {"time_zone":"Europe/Paris"}) | keep b; +evalDateParseWithTimezoneOption +required_capability: date_parse_options +row a = "10-10-2025" | eval b = date_parse("dd-mm-yyyy", a, {"time_zone":"Europe/Paris"}) | keep b; b:datetime 2024-12-31T23:00:00.000Z ; -evalDateParseWithLocale -row a = "10 septembre 2025" | eval b = date_parse(a, "dd-mm-yyyy", {"locale":"fr"}) | keep b; +evalDateParseWithLocaleOption +required_capability: date_parse_options +row a = "10 septembre 2025" | eval b = date_parse("dd MMMM yyyy", a, {"locale":"fr"}) | keep b; b:datetime 2025-09-10T00:00:00.000Z ; -evalDateParseWithLocaleAndTimezone -row a = "10 septembre 2025" | eval b = date_parse(a, "dd-mm-yyyy", {"locale":"fr","time_zone":"Europe/Paris"}) | keep b; +evalDateParseWithLocaleAndTimezoneOption +required_capability: date_parse_options +row a = "10 septembre 2025" | eval b = date_parse("dd MMMM yyyy", a, {"locale":"fr","time_zone":"Europe/Paris"}) | keep b; b:datetime 2025-09-09T22:00:00.000Z 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 df06a1109ecc9..243dcb94bcaad 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 @@ -1516,6 +1516,11 @@ public enum Cap { */ FIX_FILTER_ORDINALS, + /** + * Optional options argument for DATE_PARSE + */ + DATE_PARSE_OPTIONS, + ; private final boolean enabled; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java index b1a7f79bd712f..81bded3a789a0 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/EsqlFunctionRegistry.java @@ -1042,10 +1042,15 @@ protected static FunctionDefinition def(Class function, throw new QlIllegalArgumentException("expects one, two or three arguments"); } else if (hasMinimumTwo && (children.size() > 3 || children.size() < 2)) { throw new QlIllegalArgumentException("expects two or three arguments"); - } else if (hasMinimumTwo == false && children.size() != 3) { + } else if (hasMinimumOne == false && hasMinimumTwo == false && children.size() != 3) { throw new QlIllegalArgumentException("expects exactly three arguments"); } - return ctorRef.build(source, children.get(0), children.get(1), children.size() == 3 ? children.get(2) : null); + return ctorRef.build( + source, + children.get(0), + children.size() > 1 ? children.get(1) : null, + children.size() == 3 ? children.get(2) : null + ); }; return def(function, builder, names); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java index 1f67b9ebec99a..9fb0d180f61df 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java @@ -97,7 +97,7 @@ public DateParse( valueHint = { "standard" }, description = "The locale to use when parsing the date, relevant when parsing month names or week days." ) }, - description = "(Optional) Additional options for date parsing as <>.", + description = "(Optional) Additional options for date parsing, specifying time zone and locale as <>.", optional = true ) Expression options ) { @@ -134,7 +134,6 @@ public void writeTo(StreamOutput out) throws IOException { out.writeNamedWriteable(children().get(0)); out.writeOptionalNamedWriteable(children().size() > 1 ? children().get(1) : null); out.writeOptionalNamedWriteable(children().size() > 2 ? children().get(2) : null); - out.writeOptionalNamedWriteable(children().size() > 3 ? children().get(3) : null); } @Override 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 bfa6ec852f763..c9f5f94a3acdc 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 @@ -167,10 +167,10 @@ public void testInvalidPattern() { public void testInvalidLocale() { String pattern = "YYYY"; - String locale = "NON-EXISTING-LOCALE"; + String locale = "nonexistinglocale"; DriverContext driverContext = driverContext(); - InvalidArgumentException e = expectThrows( - InvalidArgumentException.class, + IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> evaluator( new DateParse( Source.EMPTY, @@ -193,8 +193,8 @@ public void testInvalidTimezone() { String pattern = "YYYY"; String timezone = "NON-EXISTING-TIMEZONE"; DriverContext driverContext = driverContext(); - InvalidArgumentException e = expectThrows( - InvalidArgumentException.class, + IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, () -> evaluator( new DateParse( Source.EMPTY, From e12c3325d6641b3be0c84f59c3dee11a8049290b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 17 Oct 2025 13:19:43 +0200 Subject: [PATCH 6/9] fix checkstyle violation --- .../xpack/esql/expression/function/scalar/date/DateParse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java index 9fb0d180f61df..b573af8b96b6a 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java @@ -97,7 +97,8 @@ public DateParse( valueHint = { "standard" }, description = "The locale to use when parsing the date, relevant when parsing month names or week days." ) }, - description = "(Optional) Additional options for date parsing, specifying time zone and locale as <>.", + description = "(Optional) Additional options for date parsing, specifying time zone and locale" + + "as <>.", optional = true ) Expression options ) { From f4e4518d4412aeeee1bb28dc492316e50e159c32 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 17 Oct 2025 13:21:42 +0200 Subject: [PATCH 7/9] Update docs/changelog/136548.yaml --- docs/changelog/136548.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/changelog/136548.yaml diff --git a/docs/changelog/136548.yaml b/docs/changelog/136548.yaml new file mode 100644 index 0000000000000..1c360c8e3cb49 --- /dev/null +++ b/docs/changelog/136548.yaml @@ -0,0 +1,6 @@ +pr: 136548 +summary: Locale and timezone argument for `date_parse` +area: ES|QL +type: enhancement +issues: + - 132487 From f3187d848f35e366ba6b7f3d5875ce88913678b1 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Fri, 17 Oct 2025 11:28:40 +0000 Subject: [PATCH 8/9] [CI] Auto commit changes from spotless --- .../xpack/esql/expression/function/scalar/date/DateParse.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java index b573af8b96b6a..7c39ca02fcf76 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java @@ -97,8 +97,8 @@ public DateParse( valueHint = { "standard" }, description = "The locale to use when parsing the date, relevant when parsing month names or week days." ) }, - description = "(Optional) Additional options for date parsing, specifying time zone and locale" + - "as <>.", + description = "(Optional) Additional options for date parsing, specifying time zone and locale" + + "as <>.", optional = true ) Expression options ) { From 435bd9839f7f2a1d1178a6244e0744858a93babc Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 17 Oct 2025 14:26:46 +0200 Subject: [PATCH 9/9] missing space --- .../xpack/esql/expression/function/scalar/date/DateParse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java index b573af8b96b6a..2d5ac4de6324f 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateParse.java @@ -97,7 +97,7 @@ public DateParse( valueHint = { "standard" }, description = "The locale to use when parsing the date, relevant when parsing month names or week days." ) }, - description = "(Optional) Additional options for date parsing, specifying time zone and locale" + + description = "(Optional) Additional options for date parsing, specifying time zone and locale " + "as <>.", optional = true ) Expression options