Skip to content
5 changes: 5 additions & 0 deletions docs/changelog/138695.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 138695
summary: Add `time_zone` request param support to KQL and QSTR functions
area: ES|QL
type: feature
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,40 @@ FROM logs
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
;

kqlWithTimeZoneSetting
required_capability: kql_function
required_capability: kql_function_options
required_capability: kql_qstr_timezone_support

SET time_zone = "America/New_York"\;
FROM logs
| WHERE KQL("@timestamp > \"2023-10-23T09:56:00\" AND @timestamp < \"2023-10-23T09:57:00\"")
| KEEP @timestamp, message
| SORT @timestamp ASC
;

@timestamp:date | message:text
2023-10-23T13:56:01.543Z | No response
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
;

kqlWithTimeZoneSettingWithOptionOverride
required_capability: kql_function
required_capability: kql_function_options
required_capability: kql_qstr_timezone_support

SET time_zone = "Europe/Madrid"\;
FROM logs
| WHERE KQL("@timestamp > \"2023-10-23T09:56:00\" AND @timestamp < \"2023-10-23T09:57:00\"", {"time_zone": "America/New_York"})
| KEEP @timestamp, message
| SORT @timestamp ASC
;

@timestamp:date | message:text
2023-10-23T13:56:01.543Z | No response
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
;

kqlWithDefaultFieldOption
required_capability: kql_function
required_capability: kql_function_options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,52 @@ c: long | scalerank: long
10 | 3
15 | 2
;

qstrWithTimeZoneOption
required_capability: qstr_function
required_capability: query_string_function_options

FROM logs
| WHERE QSTR("@timestamp:[2023-10-23T09:56:01 TO 2023-10-23T09:56:02]", {"time_zone": "America/New_York"})
| KEEP @timestamp, message
| SORT @timestamp ASC
;

@timestamp:date | message:text
2023-10-23T13:56:01.543Z | No response
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
;

qstrWithTimeZoneSetting
required_capability: qstr_function
required_capability: query_string_function_options
required_capability: kql_qstr_timezone_support

SET time_zone = "America/New_York"\;
FROM logs
| WHERE QSTR("@timestamp:[2023-10-23T09:56:01 TO 2023-10-23T09:56:02]")
| KEEP @timestamp, message
| SORT @timestamp ASC
;

@timestamp:date | message:text
2023-10-23T13:56:01.543Z | No response
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
;

qstrWithTimeZoneSettingWithOptionOverride
required_capability: qstr_function
required_capability: query_string_function_options
required_capability: kql_qstr_timezone_support

SET time_zone = "Europe/Madrid"\;
FROM logs
| WHERE QSTR("@timestamp:[2023-10-23T09:56:01 TO 2023-10-23T09:56:02]", {"time_zone": "America/New_York"})
| KEEP @timestamp, message
| SORT @timestamp ASC
;

@timestamp:date | message:text
2023-10-23T13:56:01.543Z | No response
2023-10-23T13:56:01.544Z | Running cats (cycle 2)
;
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,11 @@ public enum Cap {
*/
DATE_DIFF_TIMEZONE_SUPPORT(Build.current().isSnapshot()),

/**
* Support timezones in KQL and QSTR.
*/
KQL_QSTR_TIMEZONE_SUPPORT(Build.current().isSnapshot()),

/**
* (Re)Added EXPLAIN command
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,11 +524,11 @@ private static FunctionDefinition[][] functions() {
// fulltext functions
new FunctionDefinition[] {
def(Decay.class, quad(Decay::new), "decay"),
def(Kql.class, bi(Kql::new), "kql"),
def(Kql.class, bic(Kql::new), "kql"),
def(Knn.class, tri(Knn::new), "knn"),
def(Match.class, tri(Match::new), "match"),
def(MultiMatch.class, MultiMatch::new, "multi_match"),
def(QueryString.class, bi(QueryString::new), "qstr"),
def(QueryString.class, bic(QueryString::new), "qstr"),
def(MatchPhrase.class, tri(MatchPhrase::new), "match_phrase"),
def(Score.class, uni(Score::new), "score") },
// time-series functions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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

import org.apache.lucene.util.BytesRef;
import org.elasticsearch.common.lucene.BytesRefs;
import org.elasticsearch.xpack.esql.core.InvalidArgumentException;
import org.elasticsearch.xpack.esql.core.expression.EntryExpression;
import org.elasticsearch.xpack.esql.core.expression.Expression;
Expand Down Expand Up @@ -124,7 +124,7 @@ public static void populateMap(
}

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

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

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

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

// valueExpr could be a MapExpression, but for now functions only accept literal values in options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.elasticsearch.xpack.esql.core.tree.Source;
import org.elasticsearch.xpack.esql.core.type.DataType;
import org.elasticsearch.xpack.esql.expression.Foldables;
import org.elasticsearch.xpack.esql.expression.function.ConfigurationFunction;
import org.elasticsearch.xpack.esql.expression.function.Example;
import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo;
import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle;
Expand All @@ -32,8 +33,10 @@
import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates;
import org.elasticsearch.xpack.esql.planner.TranslatorHandler;
import org.elasticsearch.xpack.esql.querydsl.query.KqlQuery;
import org.elasticsearch.xpack.esql.session.Configuration;

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

private final Configuration configuration;

// Options for KQL function. They don't need to be serialized as the data nodes will retrieve them from the query builder
private final transient Expression options;

Expand Down Expand Up @@ -123,13 +128,15 @@ public Kql(
description = "Floating point number used to decrease or increase the relevance scores of the query. Defaults to 1.0."
) },
optional = true
) Expression options
) Expression options,
Configuration configuration
) {
this(source, queryString, options, null);
this(source, queryString, options, null, configuration);
}

public Kql(Source source, Expression queryString, Expression options, QueryBuilder queryBuilder) {
public Kql(Source source, Expression queryString, Expression options, QueryBuilder queryBuilder, Configuration configuration) {
super(source, queryString, options == null ? List.of(queryString) : List.of(queryString, options), queryBuilder);
this.configuration = configuration;
this.options = options;
}

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

@Override
Expand Down Expand Up @@ -183,23 +190,26 @@ protected TypeResolution resolveParams() {
}

private Map<String, Object> kqlQueryOptions() throws InvalidArgumentException {
if (options() == null) {
if (options() == null && configuration.zoneId().equals(ZoneOffset.UTC)) {
return null;
}

Map<String, Object> kqlOptions = new HashMap<>();
Options.populateMap((MapExpression) options(), kqlOptions, source(), SECOND, ALLOWED_OPTIONS);
if (options() != null) {
Options.populateMap((MapExpression) options(), kqlOptions, source(), SECOND, ALLOWED_OPTIONS);
}
kqlOptions.putIfAbsent(TIME_ZONE_FIELD.getPreferredName(), configuration.zoneId().getId());
return kqlOptions;
}

@Override
public Expression replaceChildren(List<Expression> newChildren) {
return new Kql(source(), newChildren.get(0), newChildren.size() > 1 ? newChildren.get(1) : null, queryBuilder());
return new Kql(source(), newChildren.get(0), newChildren.size() > 1 ? newChildren.get(1) : null, queryBuilder(), configuration);
}

@Override
protected NodeInfo<? extends Expression> info() {
return NodeInfo.create(this, Kql::new, query(), options(), queryBuilder());
return NodeInfo.create(this, Kql::new, query(), options(), queryBuilder(), configuration);
}

@Override
Expand All @@ -209,7 +219,7 @@ protected Query translate(LucenePushdownPredicates pushdownPredicates, Translato

@Override
public Expression replaceQueryBuilder(QueryBuilder queryBuilder) {
return new Kql(source(), query(), options(), queryBuilder);
return new Kql(source(), query(), options(), queryBuilder, configuration);
}

@Override
Expand All @@ -218,12 +228,14 @@ public boolean equals(Object o) {
// ignore options when comparing.
if (o == null || getClass() != o.getClass()) return false;
var kql = (Kql) o;
return Objects.equals(query(), kql.query()) && Objects.equals(queryBuilder(), kql.queryBuilder());
return Objects.equals(query(), kql.query())
&& Objects.equals(queryBuilder(), kql.queryBuilder())
&& Objects.equals(configuration, kql.configuration);
}

@Override
public int hashCode() {
return Objects.hash(query(), queryBuilder());
return Objects.hash(query(), queryBuilder(), configuration);
}

@Override
Expand Down
Loading