Skip to content

Commit 93d9a0b

Browse files
authored
ESQL: Add time_zone request param support to KQL and QSTR functions (#138695)
In absence of an explicit time_zone parameter, the KQL and QSTR functions now use the configuration zone instead, which defaults to "Z".
1 parent 28d5b39 commit 93d9a0b

File tree

13 files changed

+167
-42
lines changed

13 files changed

+167
-42
lines changed

docs/changelog/138695.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 138695
2+
summary: Add `time_zone` request param support to KQL and QSTR functions
3+
area: ES|QL
4+
type: feature
5+
issues: []

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,40 @@ FROM logs
330330
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
331331
;
332332

333+
kqlWithTimeZoneSetting
334+
required_capability: kql_function
335+
required_capability: kql_function_options
336+
required_capability: kql_qstr_timezone_support
337+
338+
SET time_zone = "America/New_York"\;
339+
FROM logs
340+
| WHERE KQL("@timestamp > \"2023-10-23T09:56:00\" AND @timestamp < \"2023-10-23T09:57:00\"")
341+
| KEEP @timestamp, message
342+
| SORT @timestamp ASC
343+
;
344+
345+
@timestamp:date | message:text
346+
2023-10-23T13:56:01.543Z | No response
347+
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
348+
;
349+
350+
kqlWithTimeZoneSettingWithOptionOverride
351+
required_capability: kql_function
352+
required_capability: kql_function_options
353+
required_capability: kql_qstr_timezone_support
354+
355+
SET time_zone = "Europe/Madrid"\;
356+
FROM logs
357+
| WHERE KQL("@timestamp > \"2023-10-23T09:56:00\" AND @timestamp < \"2023-10-23T09:57:00\"", {"time_zone": "America/New_York"})
358+
| KEEP @timestamp, message
359+
| SORT @timestamp ASC
360+
;
361+
362+
@timestamp:date | message:text
363+
2023-10-23T13:56:01.543Z | No response
364+
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
365+
;
366+
333367
kqlWithDefaultFieldOption
334368
required_capability: kql_function
335369
required_capability: kql_function_options

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,52 @@ c: long | scalerank: long
318318
10 | 3
319319
15 | 2
320320
;
321+
322+
qstrWithTimeZoneOption
323+
required_capability: qstr_function
324+
required_capability: query_string_function_options
325+
326+
FROM logs
327+
| WHERE QSTR("@timestamp:[2023-10-23T09:56:01 TO 2023-10-23T09:56:02]", {"time_zone": "America/New_York"})
328+
| KEEP @timestamp, message
329+
| SORT @timestamp ASC
330+
;
331+
332+
@timestamp:date | message:text
333+
2023-10-23T13:56:01.543Z | No response
334+
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
335+
;
336+
337+
qstrWithTimeZoneSetting
338+
required_capability: qstr_function
339+
required_capability: query_string_function_options
340+
required_capability: kql_qstr_timezone_support
341+
342+
SET time_zone = "America/New_York"\;
343+
FROM logs
344+
| WHERE QSTR("@timestamp:[2023-10-23T09:56:01 TO 2023-10-23T09:56:02]")
345+
| KEEP @timestamp, message
346+
| SORT @timestamp ASC
347+
;
348+
349+
@timestamp:date | message:text
350+
2023-10-23T13:56:01.543Z | No response
351+
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
352+
;
353+
354+
qstrWithTimeZoneSettingWithOptionOverride
355+
required_capability: qstr_function
356+
required_capability: query_string_function_options
357+
required_capability: kql_qstr_timezone_support
358+
359+
SET time_zone = "Europe/Madrid"\;
360+
FROM logs
361+
| WHERE QSTR("@timestamp:[2023-10-23T09:56:01 TO 2023-10-23T09:56:02]", {"time_zone": "America/New_York"})
362+
| KEEP @timestamp, message
363+
| SORT @timestamp ASC
364+
;
365+
366+
@timestamp:date | message:text
367+
2023-10-23T13:56:01.543Z | No response
368+
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
369+
;

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,11 @@ public enum Cap {
13131313
*/
13141314
DATE_DIFF_TIMEZONE_SUPPORT(Build.current().isSnapshot()),
13151315

1316+
/**
1317+
* Support timezones in KQL and QSTR.
1318+
*/
1319+
KQL_QSTR_TIMEZONE_SUPPORT(Build.current().isSnapshot()),
1320+
13161321
/**
13171322
* (Re)Added EXPLAIN command
13181323
*/

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -524,11 +524,11 @@ private static FunctionDefinition[][] functions() {
524524
// fulltext functions
525525
new FunctionDefinition[] {
526526
def(Decay.class, quad(Decay::new), "decay"),
527-
def(Kql.class, bi(Kql::new), "kql"),
527+
def(Kql.class, bic(Kql::new), "kql"),
528528
def(Knn.class, tri(Knn::new), "knn"),
529529
def(Match.class, tri(Match::new), "match"),
530530
def(MultiMatch.class, MultiMatch::new, "multi_match"),
531-
def(QueryString.class, bi(QueryString::new), "qstr"),
531+
def(QueryString.class, bic(QueryString::new), "qstr"),
532532
def(MatchPhrase.class, tri(MatchPhrase::new), "match_phrase"),
533533
def(Score.class, uni(Score::new), "score") },
534534
// time-series functions

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
package org.elasticsearch.xpack.esql.expression.function;
99

10-
import org.apache.lucene.util.BytesRef;
10+
import org.elasticsearch.common.lucene.BytesRefs;
1111
import org.elasticsearch.xpack.esql.core.InvalidArgumentException;
1212
import org.elasticsearch.xpack.esql.core.expression.EntryExpression;
1313
import org.elasticsearch.xpack.esql.core.expression.Expression;
@@ -124,7 +124,7 @@ public static void populateMap(
124124
}
125125

126126
Object optionExprLiteral = ((Literal) optionExpr).value();
127-
String optionName = optionExprLiteral instanceof BytesRef br ? br.utf8ToString() : optionExprLiteral.toString();
127+
String optionName = BytesRefs.toString(optionExprLiteral);
128128
DataType dataType = allowedOptions.get(optionName);
129129

130130
// valueExpr could be a MapExpression, but for now functions only accept literal values in options
@@ -135,7 +135,7 @@ public static void populateMap(
135135
}
136136

137137
Object valueExprLiteral = ((Literal) valueExpr).value();
138-
String optionValue = valueExprLiteral instanceof BytesRef br ? br.utf8ToString() : valueExprLiteral.toString();
138+
String optionValue = BytesRefs.toString(valueExprLiteral);
139139
// validate the optionExpr is supported
140140
if (dataType == null) {
141141
throw new InvalidArgumentException(
@@ -173,7 +173,7 @@ public static void populateMapWithExpressionsMultipleDataTypesAllowed(
173173
}
174174

175175
Object optionExprLiteral = ((Literal) optionExpr).value();
176-
String optionName = optionExprLiteral instanceof BytesRef br ? br.utf8ToString() : optionExprLiteral.toString();
176+
String optionName = BytesRefs.toString(optionExprLiteral);
177177
Collection<DataType> allowedDataTypes = allowedOptions.get(optionName);
178178

179179
// valueExpr could be a MapExpression, but for now functions only accept literal values in options

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

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.elasticsearch.xpack.esql.core.tree.Source;
2121
import org.elasticsearch.xpack.esql.core.type.DataType;
2222
import org.elasticsearch.xpack.esql.expression.Foldables;
23+
import org.elasticsearch.xpack.esql.expression.function.ConfigurationFunction;
2324
import org.elasticsearch.xpack.esql.expression.function.Example;
2425
import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo;
2526
import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle;
@@ -32,8 +33,10 @@
3233
import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates;
3334
import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
3435
import org.elasticsearch.xpack.esql.querydsl.query.KqlQuery;
36+
import org.elasticsearch.xpack.esql.session.Configuration;
3537

3638
import java.io.IOException;
39+
import java.time.ZoneOffset;
3740
import java.util.HashMap;
3841
import java.util.List;
3942
import java.util.Map;
@@ -58,9 +61,11 @@
5861
/**
5962
* Full text function that performs a {@link KqlQuery} .
6063
*/
61-
public class Kql extends FullTextFunction implements OptionalArgument {
64+
public class Kql extends FullTextFunction implements OptionalArgument, ConfigurationFunction {
6265
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Kql", Kql::readFrom);
6366

67+
private final Configuration configuration;
68+
6469
// Options for KQL function. They don't need to be serialized as the data nodes will retrieve them from the query builder
6570
private final transient Expression options;
6671

@@ -123,13 +128,15 @@ public Kql(
123128
description = "Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0."
124129
) },
125130
optional = true
126-
) Expression options
131+
) Expression options,
132+
Configuration configuration
127133
) {
128-
this(source, queryString, options, null);
134+
this(source, queryString, options, null, configuration);
129135
}
130136

131-
public Kql(Source source, Expression queryString, Expression options, QueryBuilder queryBuilder) {
137+
public Kql(Source source, Expression queryString, Expression options, QueryBuilder queryBuilder, Configuration configuration) {
132138
super(source, queryString, options == null ? List.of(queryString) : List.of(queryString, options), queryBuilder);
139+
this.configuration = configuration;
133140
this.options = options;
134141
}
135142

@@ -141,7 +148,7 @@ private static Kql readFrom(StreamInput in) throws IOException {
141148
queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class);
142149
}
143150
// Options are not serialized - they're embedded in the QueryBuilder
144-
return new Kql(source, query, null, queryBuilder);
151+
return new Kql(source, query, null, queryBuilder, ((PlanStreamInput) in).configuration());
145152
}
146153

147154
@Override
@@ -183,23 +190,26 @@ protected TypeResolution resolveParams() {
183190
}
184191

185192
private Map<String, Object> kqlQueryOptions() throws InvalidArgumentException {
186-
if (options() == null) {
193+
if (options() == null && configuration.zoneId().equals(ZoneOffset.UTC)) {
187194
return null;
188195
}
189196

190197
Map<String, Object> kqlOptions = new HashMap<>();
191-
Options.populateMap((MapExpression) options(), kqlOptions, source(), SECOND, ALLOWED_OPTIONS);
198+
if (options() != null) {
199+
Options.populateMap((MapExpression) options(), kqlOptions, source(), SECOND, ALLOWED_OPTIONS);
200+
}
201+
kqlOptions.putIfAbsent(TIME_ZONE_FIELD.getPreferredName(), configuration.zoneId().getId());
192202
return kqlOptions;
193203
}
194204

195205
@Override
196206
public Expression replaceChildren(List<Expression> newChildren) {
197-
return new Kql(source(), newChildren.get(0), newChildren.size() > 1 ? newChildren.get(1) : null, queryBuilder());
207+
return new Kql(source(), newChildren.get(0), newChildren.size() > 1 ? newChildren.get(1) : null, queryBuilder(), configuration);
198208
}
199209

200210
@Override
201211
protected NodeInfo<? extends Expression> info() {
202-
return NodeInfo.create(this, Kql::new, query(), options(), queryBuilder());
212+
return NodeInfo.create(this, Kql::new, query(), options(), queryBuilder(), configuration);
203213
}
204214

205215
@Override
@@ -209,7 +219,7 @@ protected Query translate(LucenePushdownPredicates pushdownPredicates, Translato
209219

210220
@Override
211221
public Expression replaceQueryBuilder(QueryBuilder queryBuilder) {
212-
return new Kql(source(), query(), options(), queryBuilder);
222+
return new Kql(source(), query(), options(), queryBuilder, configuration);
213223
}
214224

215225
@Override
@@ -218,12 +228,14 @@ public boolean equals(Object o) {
218228
// ignore options when comparing.
219229
if (o == null || getClass() != o.getClass()) return false;
220230
var kql = (Kql) o;
221-
return Objects.equals(query(), kql.query()) && Objects.equals(queryBuilder(), kql.queryBuilder());
231+
return Objects.equals(query(), kql.query())
232+
&& Objects.equals(queryBuilder(), kql.queryBuilder())
233+
&& Objects.equals(configuration, kql.configuration);
222234
}
223235

224236
@Override
225237
public int hashCode() {
226-
return Objects.hash(query(), queryBuilder());
238+
return Objects.hash(query(), queryBuilder(), configuration);
227239
}
228240

229241
@Override

0 commit comments

Comments
 (0)