Skip to content

Commit 0f39275

Browse files
committed
work work work
1 parent 104648b commit 0f39275

File tree

8 files changed

+153
-55
lines changed

8 files changed

+153
-55
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,27 @@ emp_no:integer | new_date:datetime | birth_date:datetime | bool:
644644
10050 | 1958-05-21T00:00:00.000Z | 1958-05-21T00:00:00.000Z | true
645645
;
646646

647+
evalDateParseWithTimezone
648+
row a = "10-10-25" | eval b = date_parse(a, "dd-mm-yyyy", {"time_zone":"Europe/Paris"}) | keep b;
649+
650+
b:datetime
651+
2024-12-31T23:00:00.000Z
652+
;
653+
654+
evalDateParseWithLocale
655+
row a = "10 septembre 2025" | eval b = date_parse(a, "dd-mm-yyyy", {"locale":"fr"}) | keep b;
656+
657+
b:datetime
658+
2025-09-10T00:00:00.000Z
659+
;
660+
661+
evalDateParseWithLocaleAndTimezone
662+
row a = "10 septembre 2025" | eval b = date_parse(a, "dd-mm-yyyy", {"locale":"fr","time_zone":"Europe/Paris"}) | keep b;
663+
664+
b:datetime
665+
2025-09-09T22:00:00.000Z
666+
;
667+
647668
dateFields
648669
from employees | where emp_no == 10049 or emp_no == 10050
649670
| eval year = date_extract("year", birth_date), month = date_extract("month_of_year", birth_date), day = date_extract("day_of_month", birth_date)

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,10 @@ public interface BinaryBuilder<T> {
10371037
protected static <T extends Function> FunctionDefinition def(Class<T> function, TernaryBuilder<T> ctorRef, String... names) {
10381038
FunctionBuilder builder = (source, children, cfg) -> {
10391039
boolean hasMinimumTwo = OptionalArgument.class.isAssignableFrom(function);
1040-
if (hasMinimumTwo && (children.size() > 3 || children.size() < 2)) {
1040+
boolean hasMinimumOne = TwoOptionalArguments.class.isAssignableFrom(function);
1041+
if (hasMinimumOne && (children.size() > 3 || children.isEmpty())) {
1042+
throw new QlIllegalArgumentException("expects one, two or three arguments");
1043+
} else if (hasMinimumTwo && (children.size() > 3 || children.size() < 2)) {
10411044
throw new QlIllegalArgumentException("expects two or three arguments");
10421045
} else if (hasMinimumTwo == false && children.size() != 3) {
10431046
throw new QlIllegalArgumentException("expects exactly three arguments");
@@ -1065,17 +1068,13 @@ protected static <T extends Function> FunctionDefinition def(Class<T> function,
10651068
if (children.size() > 4 || children.size() < 2) {
10661069
throw new QlIllegalArgumentException("expects minimum two, maximum four arguments");
10671070
}
1068-
} else if (ThreeOptionalArguments.class.isAssignableFrom(function)) {
1069-
if (children.size() > 4 || children.isEmpty()) {
1070-
throw new QlIllegalArgumentException("expects minimum one, maximum four arguments");
1071-
}
10721071
} else if (children.size() != 4) {
10731072
throw new QlIllegalArgumentException("expects exactly four arguments");
10741073
}
10751074
return ctorRef.build(
10761075
source,
10771076
children.get(0),
1078-
children.size() > 1 ? children.get(1) : null,
1077+
children.get(1),
10791078
children.size() > 2 ? children.get(2) : null,
10801079
children.size() > 3 ? children.get(3) : null
10811080
);

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

Lines changed: 0 additions & 16 deletions
This file was deleted.

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

Lines changed: 71 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,46 +12,58 @@
1212
import org.elasticsearch.common.io.stream.StreamInput;
1313
import org.elasticsearch.common.io.stream.StreamOutput;
1414
import org.elasticsearch.common.time.DateFormatter;
15+
import org.elasticsearch.common.util.LocaleUtils;
1516
import org.elasticsearch.compute.ann.Evaluator;
1617
import org.elasticsearch.compute.ann.Fixed;
1718
import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
1819
import org.elasticsearch.xpack.esql.core.InvalidArgumentException;
1920
import org.elasticsearch.xpack.esql.core.expression.Expression;
21+
import org.elasticsearch.xpack.esql.core.expression.MapExpression;
2022
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
2123
import org.elasticsearch.xpack.esql.core.tree.Source;
2224
import org.elasticsearch.xpack.esql.core.type.DataType;
2325
import org.elasticsearch.xpack.esql.expression.function.Example;
2426
import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
27+
import org.elasticsearch.xpack.esql.expression.function.MapParam;
28+
import org.elasticsearch.xpack.esql.expression.function.Options;
2529
import org.elasticsearch.xpack.esql.expression.function.Param;
26-
import org.elasticsearch.xpack.esql.expression.function.ThreeOptionalArguments;
30+
import org.elasticsearch.xpack.esql.expression.function.TwoOptionalArguments;
2731
import org.elasticsearch.xpack.esql.expression.function.scalar.EsqlScalarFunction;
2832
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
2933

3034
import java.io.IOException;
35+
import java.time.ZoneId;
36+
import java.time.zone.ZoneRulesException;
3137
import java.util.ArrayList;
38+
import java.util.HashMap;
3239
import java.util.List;
3340
import java.util.Locale;
34-
import java.util.TimeZone;
41+
import java.util.Map;
3542

43+
import static java.util.Map.entry;
3644
import static org.elasticsearch.common.time.DateFormatter.forPattern;
3745
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
3846
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
47+
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD;
3948
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isString;
49+
import static org.elasticsearch.xpack.esql.core.type.DataType.KEYWORD;
4050
import static org.elasticsearch.xpack.esql.expression.EsqlTypeResolutions.isStringAndExact;
4151
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.DEFAULT_DATE_TIME_FORMATTER;
4252
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong;
4353

44-
public class DateParse extends EsqlScalarFunction implements ThreeOptionalArguments {
54+
public class DateParse extends EsqlScalarFunction implements TwoOptionalArguments {
4555
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
4656
Expression.class,
4757
"DateParse",
4858
DateParse::new
4959
);
5060

61+
private static final String TIME_ZONE_PARAM_NAME = "time_zone";
62+
private static final String LOCALE_PARAM_NAME = "locale";
63+
5164
private final Expression field;
5265
private final Expression format;
53-
private final Expression locale;
54-
private final Expression timezone;
66+
private final Expression options;
5567

5668
@FunctionInfo(
5769
returnType = "date",
@@ -69,27 +81,40 @@ public DateParse(
6981
type = { "keyword", "text" },
7082
description = "Date expression as a string. If `null` or an empty string, the function returns `null`."
7183
) Expression second,
72-
@Param(name = "dateLocale", type = { "keyword", "text" }, description = "The locale to parse with") Expression third,
73-
@Param(name = "dateTimezone", type = { "keyword", "text" }, description = "The timezone to parse with") Expression forth
84+
@MapParam(
85+
name = "options",
86+
params = {
87+
@MapParam.MapParamEntry(
88+
name = TIME_ZONE_PARAM_NAME,
89+
type = "keyword",
90+
valueHint = { "standard" },
91+
description = "Coordinated Universal Time (UTC) offset or IANA time zone used to convert date values in the "
92+
+ "query string to UTC."
93+
),
94+
@MapParam.MapParamEntry(
95+
name = LOCALE_PARAM_NAME,
96+
type = "keyword",
97+
valueHint = { "standard" },
98+
description = "The locale to use when parsing the date, relevant when parsing month names or week days."
99+
)
100+
},
101+
description = "(Optional) Additional options for date parsing as <<esql-function-named-params,function named parameters>>.",
102+
optional = true) Expression options
74103
) {
75-
super(source, fields(first, second, third, forth));
104+
super(source, fields(first, second, options));
76105
this.field = second != null ? second : first;
77106
this.format = second != null ? first : null;
78-
this.locale = third;
79-
this.timezone = forth;
107+
this.options = options;
80108
}
81109

82-
private static List<Expression> fields(Expression field, Expression format, Expression locale, Expression timezone) {
110+
private static List<Expression> fields(Expression field, Expression format, Expression options) {
83111
List<Expression> list = new ArrayList<>(3);
84112
list.add(field);
85113
if (format != null) {
86114
list.add(format);
87115
}
88-
if (locale != null) {
89-
list.add(locale);
90-
}
91-
if (timezone != null) {
92-
list.add(timezone);
116+
if (options != null) {
117+
list.add(options);
93118
}
94119
return list;
95120
}
@@ -99,7 +124,6 @@ private DateParse(StreamInput in) throws IOException {
99124
Source.readFrom((PlanStreamInput) in),
100125
in.readNamedWriteable(Expression.class),
101126
in.readOptionalNamedWriteable(Expression.class),
102-
in.readOptionalNamedWriteable(Expression.class),
103127
in.readOptionalNamedWriteable(Expression.class)
104128
);
105129
}
@@ -160,6 +184,21 @@ static long process(BytesRef val, BytesRef formatter) throws IllegalArgumentExce
160184
return dateTimeToLong(val.utf8ToString(), toFormatter(formatter));
161185
}
162186

187+
public static final Map<String, DataType> ALLOWED_OPTIONS = Map.ofEntries(
188+
entry(TIME_ZONE_PARAM_NAME, KEYWORD),
189+
entry(LOCALE_PARAM_NAME, KEYWORD)
190+
);
191+
192+
private Map<String, Object> parseOptions() throws InvalidArgumentException {
193+
Map<String, Object> matchOptions = new HashMap<>();
194+
if (this.options == null) {
195+
return matchOptions;
196+
}
197+
198+
Options.populateMap((MapExpression) this.options, matchOptions, source(), THIRD, ALLOWED_OPTIONS);
199+
return matchOptions;
200+
}
201+
163202
@Override
164203
public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
165204
ExpressionEvaluator.Factory fieldEvaluator = toEvaluator.apply(field);
@@ -169,22 +208,28 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
169208
if (DataType.isString(format.dataType()) == false) {
170209
throw new IllegalArgumentException("unsupported data type for date_parse [" + format.dataType() + "]");
171210
}
172-
String localeAsString = locale == null ? null : ((BytesRef) locale.fold(toEvaluator.foldCtx())).utf8ToString();
173-
Locale locale = localeAsString == null ? null : Locale.forLanguageTag(localeAsString);
174-
if (localeAsString != null && locale == null) {
175-
throw new IllegalArgumentException("unsupported locale [" + localeAsString + "]");
211+
var parsedOptions = this.parseOptions();
212+
String localeAsString = (String)parsedOptions.get(LOCALE_PARAM_NAME);
213+
Locale locale = localeAsString == null ? null : LocaleUtils.parse(localeAsString);
214+
215+
String timezoneAsString = (String)parsedOptions.get(TIME_ZONE_PARAM_NAME);
216+
ZoneId timezone = null;
217+
try {
218+
if (timezoneAsString != null) {
219+
timezone = ZoneId.of(timezoneAsString);
220+
}
221+
} catch (ZoneRulesException e) {
222+
throw new IllegalArgumentException("unsupported timezone [" + timezoneAsString + "]");
176223
}
177224

178-
String timezoneAsString = timezone == null ? null : ((BytesRef) timezone.fold(toEvaluator.foldCtx())).utf8ToString();
179-
TimeZone timezone = timezoneAsString == null ? null : TimeZone.getTimeZone(timezoneAsString);
180225
if (format.foldable()) {
181226
try {
182227
DateFormatter formatter = toFormatter(format.fold(toEvaluator.foldCtx()));
183228
if (locale != null) {
184229
formatter = formatter.withLocale(locale);
185230
}
186231
if (timezone != null) {
187-
formatter = formatter.withZone(timezone.toZoneId());
232+
formatter = formatter.withZone(timezone);
188233
}
189234
return new DateParseConstantEvaluator.Factory(source(), fieldEvaluator, formatter);
190235
} catch (IllegalArgumentException e) {
@@ -205,15 +250,14 @@ public Expression replaceChildren(List<Expression> newChildren) {
205250
source(),
206251
newChildren.get(0),
207252
newChildren.size() > 1 ? newChildren.get(1) : null,
208-
newChildren.size() > 2 ? newChildren.get(2) : null,
209-
newChildren.size() > 3 ? newChildren.get(3) : null
253+
newChildren.size() > 2 ? newChildren.get(2) : null
210254
);
211255
}
212256

213257
@Override
214258
protected NodeInfo<? extends Expression> info() {
215259
Expression first = format != null ? format : field;
216260
Expression second = format != null ? field : null;
217-
return NodeInfo.create(this, DateParse::new, first, second, locale, timezone);
261+
return NodeInfo.create(this, DateParse::new, first, second, options);
218262
}
219263
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ protected List<TestCaseSupplier> cases() {
2727

2828
@Override
2929
protected Expression build(Source source, List<Expression> args) {
30-
return new DateParse(source, args.get(0), args.size() > 1 ? args.get(1) : null);
30+
return new DateParse(source, args.get(0), args.size() > 1 ? args.get(1) : null, args.size() == 3 ? args.get(2) : null);
3131
}
3232

3333
@Override

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ protected DateParse createTestInstance() {
1919
Source source = randomSource();
2020
Expression first = randomChild();
2121
Expression second = randomBoolean() ? null : randomChild();
22-
return new DateParse(source, first, second);
22+
Expression options = second != null && randomBoolean() ? randomChild() : null;
23+
return new DateParse(source, first, second, options);
2324
}
2425

2526
@Override
@@ -32,6 +33,11 @@ protected DateParse mutateInstance(DateParse instance) throws IOException {
3233
} else {
3334
second = randomValueOtherThan(second, () -> randomBoolean() ? null : randomChild());
3435
}
35-
return new DateParse(source, first, second);
36+
Expression options = instance.children().size() == 3 ? instance.children().get(2) : null;
37+
38+
if (randomBoolean()) {
39+
options = randomValueOtherThan(first, AbstractExpressionSerializationTests::randomChild);
40+
}
41+
return new DateParse(source, first, second, options);
3642
}
3743
}

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.elasticsearch.xpack.esql.core.InvalidArgumentException;
1616
import org.elasticsearch.xpack.esql.core.expression.Expression;
1717
import org.elasticsearch.xpack.esql.core.expression.Literal;
18+
import org.elasticsearch.xpack.esql.core.expression.MapExpression;
1819
import org.elasticsearch.xpack.esql.core.tree.Source;
1920
import org.elasticsearch.xpack.esql.core.type.DataType;
2021
import org.elasticsearch.xpack.esql.expression.function.AbstractScalarFunctionTestCase;
@@ -156,15 +157,58 @@ public void testInvalidPattern() {
156157
new DateParse(
157158
Source.EMPTY,
158159
new Literal(Source.EMPTY, new BytesRef(pattern), DataType.KEYWORD),
159-
field("str", DataType.KEYWORD)
160+
field("str", DataType.KEYWORD),
161+
null
160162
)
161163
).get(driverContext)
162164
);
163165
assertThat(e.getMessage(), startsWith("invalid date pattern for []: Invalid format: [" + pattern + "]"));
164166
}
165167

168+
public void testInvalidLocale() {
169+
String pattern = "YYYY";
170+
String locale = "NON-EXISTING-LOCALE";
171+
DriverContext driverContext = driverContext();
172+
InvalidArgumentException e = expectThrows(
173+
InvalidArgumentException.class,
174+
() -> evaluator(
175+
new DateParse(
176+
Source.EMPTY,
177+
new Literal(Source.EMPTY, new BytesRef(pattern), DataType.KEYWORD),
178+
field("str", DataType.KEYWORD),
179+
new MapExpression(Source.EMPTY, List.of(
180+
new Literal(Source.EMPTY, new BytesRef("locale"), DataType.KEYWORD),
181+
new Literal(Source.EMPTY, new BytesRef(locale), DataType.KEYWORD)
182+
))
183+
)
184+
).get(driverContext)
185+
);
186+
assertThat(e.getMessage(), startsWith("Unknown language: " + locale));
187+
}
188+
189+
public void testInvalidTimezone() {
190+
String pattern = "YYYY";
191+
String timezone = "NON-EXISTING-TIMEZONE";
192+
DriverContext driverContext = driverContext();
193+
InvalidArgumentException e = expectThrows(
194+
InvalidArgumentException.class,
195+
() -> evaluator(
196+
new DateParse(
197+
Source.EMPTY,
198+
new Literal(Source.EMPTY, new BytesRef(pattern), DataType.KEYWORD),
199+
field("str", DataType.KEYWORD),
200+
new MapExpression(Source.EMPTY, List.of(
201+
new Literal(Source.EMPTY, new BytesRef("time_zone"), DataType.KEYWORD),
202+
new Literal(Source.EMPTY, new BytesRef(timezone), DataType.KEYWORD)
203+
))
204+
)
205+
).get(driverContext)
206+
);
207+
assertThat(e.getMessage(), startsWith("unsupported timezone [" + timezone + "]"));
208+
}
209+
166210
@Override
167211
protected Expression build(Source source, List<Expression> args) {
168-
return new DateParse(source, args.get(0), args.size() > 1 ? args.get(1) : null);
212+
return new DateParse(source, args.get(0), args.size() > 1 ? args.get(1) : null, args.size() == 3 ? args.get(2) : null);
169213
}
170214
}

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
@@ -97,7 +97,7 @@ public void testBasicNullFolding() {
9797
assertNullLiteral(foldNull(new Round(EMPTY, Literal.NULL, null)));
9898
assertNullLiteral(foldNull(new Pow(EMPTY, Literal.NULL, Literal.NULL)));
9999
assertNullLiteral(foldNull(new DateFormat(EMPTY, Literal.NULL, Literal.NULL, null)));
100-
assertNullLiteral(foldNull(new DateParse(EMPTY, Literal.NULL, Literal.NULL)));
100+
assertNullLiteral(foldNull(new DateParse(EMPTY, Literal.NULL, Literal.NULL, NULL)));
101101
assertNullLiteral(foldNull(new DateTrunc(EMPTY, Literal.NULL, Literal.NULL)));
102102
assertNullLiteral(foldNull(new Substring(EMPTY, Literal.NULL, Literal.NULL, Literal.NULL)));
103103
}

0 commit comments

Comments
 (0)