From b47b60653d6c68db66e26dc0c04d7c422bf6ce26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 26 Nov 2025 19:05:11 +0100 Subject: [PATCH 1/5] ESQL: Add time_zone request param support to KQL and QSTR functions --- .../src/main/resources/kql-function.csv-spec | 34 +++++++++++++ .../src/main/resources/qstr-function.csv-spec | 49 ++++++++++++++++++ .../xpack/esql/action/EsqlCapabilities.java | 5 ++ .../function/EsqlFunctionRegistry.java | 4 +- .../esql/expression/function/Options.java | 8 +-- .../expression/function/fulltext/Kql.java | 39 ++++++++------ .../function/fulltext/QueryString.java | 51 ++++++++++++------- .../function/fulltext/KqlErrorTests.java | 3 +- .../function/fulltext/KqlTests.java | 4 +- .../NoneFieldFullTextFunctionTestCase.java | 2 +- .../fulltext/QueryStringErrorTests.java | 3 +- .../function/fulltext/QueryStringTests.java | 4 +- 12 files changed, 159 insertions(+), 47 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/kql-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/kql-function.csv-spec index 204d17f57ab50..9147e31397273 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/kql-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/kql-function.csv-spec @@ -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 diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec index 90902137db230..902b61649f751 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec @@ -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.543 OR @timestamp:2023-10-23T13:56:01.544", {"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.543 OR @timestamp:2023-10-23T13:56:01.544") +| 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.543 OR @timestamp:2023-10-23T13:56:01.544", {"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) +; 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 325ab9944549b..4ce538dafcd94 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 @@ -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 */ 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 c51af896ba01e..584191ffc46f3 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 @@ -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 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/Options.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/Options.java index 113c40166eace..38d0dc9761f33 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/Options.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/Options.java @@ -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; @@ -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 @@ -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( @@ -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 allowedDataTypes = allowedOptions.get(optionName); // valueExpr could be a MapExpression, but for now functions only accept literal values in options diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java index 3bc4020771238..f8786b734c540 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java @@ -12,6 +12,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.logging.LogManager; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.MapExpression; @@ -20,6 +21,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; @@ -32,6 +34,7 @@ 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.util.HashMap; @@ -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; @@ -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; } @@ -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 @@ -183,23 +190,23 @@ protected TypeResolution resolveParams() { } private Map kqlQueryOptions() throws InvalidArgumentException { - if (options() == null) { - return null; - } - Map 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()); + LogManager.getLogger(this.getClass()).error("TIME_ZONE: " + kqlOptions.get(TIME_ZONE_FIELD.getPreferredName())); return kqlOptions; } @Override public Expression replaceChildren(List 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 info() { - return NodeInfo.create(this, Kql::new, query(), options(), queryBuilder()); + return NodeInfo.create(this, Kql::new, query(), options(), queryBuilder(), configuration); } @Override @@ -209,7 +216,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 @@ -218,12 +225,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 diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java index f31a5d2564359..dd6df0a3812e2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java @@ -21,6 +21,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; @@ -32,6 +33,7 @@ import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; import org.elasticsearch.xpack.esql.optimizer.rules.physical.local.LucenePushdownPredicates; import org.elasticsearch.xpack.esql.planner.TranslatorHandler; +import org.elasticsearch.xpack.esql.session.Configuration; import java.io.IOException; import java.util.HashMap; @@ -76,7 +78,7 @@ /** * Full text function that performs a {@link QueryStringQuery} . */ -public class QueryString extends FullTextFunction implements OptionalArgument { +public class QueryString extends FullTextFunction implements OptionalArgument, ConfigurationFunction { public static final Map ALLOWED_OPTIONS = Map.ofEntries( entry(BOOST_FIELD.getPreferredName(), FLOAT), @@ -107,6 +109,11 @@ public class QueryString extends FullTextFunction implements OptionalArgument { QueryString::readFrom ); + private final Configuration configuration; + + // Options for QueryString. They don't need to be serialized as the data nodes will retrieve them from the query builder. + private final transient Expression options; + @FunctionInfo( returnType = "boolean", appliesTo = { @@ -265,16 +272,15 @@ public QueryString( description = "(Optional) Additional options for Query String as <>." + " See <> for more information.", optional = true - ) Expression options + ) Expression options, + Configuration configuration ) { - this(source, queryString, options, null); + this(source, queryString, options, null, configuration); } - // Options for QueryString. They don't need to be serialized as the data nodes will retrieve them from the query builder. - private final transient Expression options; - - public QueryString(Source source, Expression queryString, Expression options, QueryBuilder queryBuilder) { + public QueryString(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; } @@ -285,7 +291,7 @@ private static QueryString readFrom(StreamInput in) throws IOException { if (in.getTransportVersion().supports(TransportVersions.V_8_18_0)) { queryBuilder = in.readOptionalNamedWriteable(QueryBuilder.class); } - return new QueryString(source, query, null, queryBuilder); + return new QueryString(source, query, null, queryBuilder, ((PlanStreamInput) in).configuration()); } @Override @@ -328,13 +334,12 @@ private TypeResolution resolveQuery() { } private Map queryStringOptions() throws InvalidArgumentException { - if (options() == null) { - return null; + Map queryStringOptions = new HashMap<>(); + if (options() != null) { + Options.populateMap((MapExpression) options(), queryStringOptions, source(), SECOND, ALLOWED_OPTIONS); } - - Map matchOptions = new HashMap<>(); - Options.populateMap((MapExpression) options(), matchOptions, source(), SECOND, ALLOWED_OPTIONS); - return matchOptions; + queryStringOptions.putIfAbsent(TIME_ZONE_FIELD.getPreferredName(), configuration.zoneId().getId()); + return queryStringOptions; } @Override @@ -344,12 +349,18 @@ protected TypeResolution resolveParams() { @Override public Expression replaceChildren(List newChildren) { - return new QueryString(source(), newChildren.getFirst(), newChildren.size() == 1 ? null : newChildren.get(1), queryBuilder()); + return new QueryString( + source(), + newChildren.getFirst(), + newChildren.size() == 1 ? null : newChildren.get(1), + queryBuilder(), + configuration + ); } @Override protected NodeInfo info() { - return NodeInfo.create(this, QueryString::new, query(), options(), queryBuilder()); + return NodeInfo.create(this, QueryString::new, query(), options(), queryBuilder(), configuration); } @Override @@ -359,7 +370,7 @@ protected Query translate(LucenePushdownPredicates pushdownPredicates, Translato @Override public Expression replaceQueryBuilder(QueryBuilder queryBuilder) { - return new QueryString(source(), query(), options(), queryBuilder); + return new QueryString(source(), query(), options(), queryBuilder, configuration); } @Override @@ -368,11 +379,13 @@ public boolean equals(Object o) { // ignore options when comparing. if (o == null || getClass() != o.getClass()) return false; var qstr = (QueryString) o; - return Objects.equals(query(), qstr.query()) && Objects.equals(queryBuilder(), qstr.queryBuilder()); + return Objects.equals(query(), qstr.query()) + && Objects.equals(queryBuilder(), qstr.queryBuilder()) + && Objects.equals(configuration, qstr.configuration); } @Override public int hashCode() { - return Objects.hash(query(), queryBuilder()); + return Objects.hash(query(), queryBuilder(), configuration); } } diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KqlErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KqlErrorTests.java index 8377ce664a345..b0e6122b0f3dc 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KqlErrorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KqlErrorTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext; +import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -39,7 +40,7 @@ protected Stream> testCandidates(List cases, Se @Override protected Expression build(Source source, List args) { - return new Kql(source, args.getFirst(), args.size() > 1 ? args.get(1) : null); + return new Kql(source, args.getFirst(), args.size() > 1 ? args.get(1) : null, EsqlTestUtils.TEST_CFG); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KqlTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KqlTests.java index 1b34aa9fefe31..e670dfbf9af7f 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KqlTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/KqlTests.java @@ -58,7 +58,7 @@ private static List addFunctionNamedParams(List addFunctionNamedParams(List args) { - Kql kql = new Kql(source, args.get(0), args.size() > 1 ? args.get(1) : null); + Kql kql = new Kql(source, args.get(0), args.size() > 1 ? args.get(1) : null, testCase.getConfiguration()); // We need to add the QueryBuilder to the kql expression, as it is used to implement equals() and hashCode() and // thus test the serialization methods. But we can only do this if the parameters make sense. if (args.get(0).foldable()) { diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/NoneFieldFullTextFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/NoneFieldFullTextFunctionTestCase.java index 4bc228a892d08..cef5f071bf71a 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/NoneFieldFullTextFunctionTestCase.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/NoneFieldFullTextFunctionTestCase.java @@ -52,7 +52,7 @@ protected static Iterable generateParameters() { private static TestCaseSupplier.TestCase testCase(DataType strType, String str, Matcher matcher) { return new TestCaseSupplier.TestCase( List.of(new TestCaseSupplier.TypedData(new BytesRef(str), strType, "query")), - "EndsWithEvaluator[str=Attribute[channel=0], suffix=Attribute[channel=1]]", + "", DataType.BOOLEAN, matcher ); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringErrorTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringErrorTests.java index 95c8fd5446dce..13a2971c661a6 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringErrorTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringErrorTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.esql.expression.function.fulltext; +import org.elasticsearch.xpack.esql.EsqlTestUtils; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; import org.elasticsearch.xpack.esql.core.tree.Source; @@ -39,7 +40,7 @@ protected Stream> testCandidates(List cases, Se @Override protected Expression build(Source source, List args) { - return new QueryString(source, args.getFirst(), args.size() > 1 ? args.get(1) : null); + return new QueryString(source, args.getFirst(), args.size() > 1 ? args.get(1) : null, EsqlTestUtils.TEST_CFG); } @Override diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringTests.java index df8efcfc87e03..d9df2c7b260f8 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryStringTests.java @@ -64,7 +64,7 @@ private static List addFunctionNamedParams(List addFunctionNamedParams(List args) { - var qstr = new QueryString(source, args.get(0), args.size() > 1 ? args.get(1) : null); + var qstr = new QueryString(source, args.get(0), args.size() > 1 ? args.get(1) : null, testCase.getConfiguration()); // We need to add the QueryBuilder to the match expression, as it is used to implement equals() and hashCode() and // thus test the serialization methods. But we can only do this if the parameters make sense . if (args.get(0).foldable()) { From cd9b1abe3fd5077ac508c75037e4a7ae27065190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 26 Nov 2025 18:10:21 +0000 Subject: [PATCH 2/5] Update docs/changelog/138695.yaml --- docs/changelog/138695.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/138695.yaml diff --git a/docs/changelog/138695.yaml b/docs/changelog/138695.yaml new file mode 100644 index 0000000000000..71b483873e73b --- /dev/null +++ b/docs/changelog/138695.yaml @@ -0,0 +1,5 @@ +pr: 138695 +summary: Add `time_zone` request param support to KQL and QSTR functions +area: ES|QL +type: feature +issues: [] From e9f31cd34b25952068282e5f0fbeac61447b1fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 26 Nov 2025 19:11:42 +0100 Subject: [PATCH 3/5] Removed log --- .../xpack/esql/expression/function/fulltext/Kql.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java index f8786b734c540..a46c8e4578cf3 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java @@ -12,7 +12,6 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.logging.LogManager; import org.elasticsearch.xpack.esql.core.InvalidArgumentException; import org.elasticsearch.xpack.esql.core.expression.Expression; import org.elasticsearch.xpack.esql.core.expression.MapExpression; @@ -195,7 +194,6 @@ private Map kqlQueryOptions() throws InvalidArgumentException { Options.populateMap((MapExpression) options(), kqlOptions, source(), SECOND, ALLOWED_OPTIONS); } kqlOptions.putIfAbsent(TIME_ZONE_FIELD.getPreferredName(), configuration.zoneId().getId()); - LogManager.getLogger(this.getClass()).error("TIME_ZONE: " + kqlOptions.get(TIME_ZONE_FIELD.getPreferredName())); return kqlOptions; } From 1b0b4b254639e68ec9266f89eb4b837abab571ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Wed, 26 Nov 2025 19:13:39 +0100 Subject: [PATCH 4/5] Fixed tests --- .../testFixtures/src/main/resources/qstr-function.csv-spec | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec index 902b61649f751..623bd556baedc 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec @@ -324,7 +324,7 @@ required_capability: qstr_function required_capability: query_string_function_options FROM logs -| WHERE QSTR("@timestamp:2023-10-23T09:56:01.543 OR @timestamp:2023-10-23T13:56:01.544", {"time_zone": "America/New_York"}) +| WHERE QSTR("@timestamp:[2023-10-23 TO 2023-10-23]", {"time_zone": "America/New_York"}) | KEEP @timestamp, message | SORT @timestamp ASC ; @@ -341,7 +341,7 @@ required_capability: kql_qstr_timezone_support SET time_zone = "America/New_York"\; FROM logs -| WHERE QSTR("@timestamp:2023-10-23T09:56:01.543 OR @timestamp:2023-10-23T13:56:01.544") +| WHERE QSTR("@timestamp:[2023-10-23 TO 2023-10-23]") | KEEP @timestamp, message | SORT @timestamp ASC ; @@ -358,7 +358,7 @@ required_capability: kql_qstr_timezone_support SET time_zone = "Europe/Madrid"\; FROM logs -| WHERE QSTR("@timestamp:2023-10-23T09:56:01.543 OR @timestamp:2023-10-23T13:56:01.544", {"time_zone": "America/New_York"}) +| WHERE QSTR("@timestamp:[2023-10-23 TO 2023-10-23]", {"time_zone": "America/New_York"}) | KEEP @timestamp, message | SORT @timestamp ASC ; From a64239b62d597eeedd070e04cfb75569029a2a52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Cea=20Fontenla?= Date: Thu, 27 Nov 2025 13:32:40 +0100 Subject: [PATCH 5/5] Fixed qstr tests and avoid adding timezone to queries if it's Z --- .../testFixtures/src/main/resources/qstr-function.csv-spec | 6 +++--- .../xpack/esql/expression/function/fulltext/Kql.java | 5 +++++ .../esql/expression/function/fulltext/QueryString.java | 5 +++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec index 623bd556baedc..c190bbe67640c 100644 --- a/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec +++ b/x-pack/plugin/esql/qa/testFixtures/src/main/resources/qstr-function.csv-spec @@ -324,7 +324,7 @@ required_capability: qstr_function required_capability: query_string_function_options FROM logs -| WHERE QSTR("@timestamp:[2023-10-23 TO 2023-10-23]", {"time_zone": "America/New_York"}) +| 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 ; @@ -341,7 +341,7 @@ required_capability: kql_qstr_timezone_support SET time_zone = "America/New_York"\; FROM logs -| WHERE QSTR("@timestamp:[2023-10-23 TO 2023-10-23]") +| WHERE QSTR("@timestamp:[2023-10-23T09:56:01 TO 2023-10-23T09:56:02]") | KEEP @timestamp, message | SORT @timestamp ASC ; @@ -358,7 +358,7 @@ required_capability: kql_qstr_timezone_support SET time_zone = "Europe/Madrid"\; FROM logs -| WHERE QSTR("@timestamp:[2023-10-23 TO 2023-10-23]", {"time_zone": "America/New_York"}) +| 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 ; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java index a46c8e4578cf3..a0e9e3a9f6612 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/Kql.java @@ -36,6 +36,7 @@ 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; @@ -189,6 +190,10 @@ protected TypeResolution resolveParams() { } private Map kqlQueryOptions() throws InvalidArgumentException { + if (options() == null && configuration.zoneId().equals(ZoneOffset.UTC)) { + return null; + } + Map kqlOptions = new HashMap<>(); if (options() != null) { Options.populateMap((MapExpression) options(), kqlOptions, source(), SECOND, ALLOWED_OPTIONS); diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java index dd6df0a3812e2..b52404e2c9396 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/fulltext/QueryString.java @@ -36,6 +36,7 @@ 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; @@ -334,6 +335,10 @@ private TypeResolution resolveQuery() { } private Map queryStringOptions() throws InvalidArgumentException { + if (options() == null && configuration.zoneId().equals(ZoneOffset.UTC)) { + return null; + } + Map queryStringOptions = new HashMap<>(); if (options() != null) { Options.populateMap((MapExpression) options(), queryStringOptions, source(), SECOND, ALLOWED_OPTIONS);