Skip to content

Commit 8172161

Browse files
authored
ESQL: Load timezone query settings (SET x=y) into configuration (#136637)
Continuation of #136205 - Load the timezone query setting to the Configuration (Snapshot only, as SET is snapshot) - Add a time_zone request body param as a fallback (Snapshot only) The `Configuration.zoneId` field where this is being stored is already used by 3 functions: - `DateExtract` - `DayName` - `MonthName` And also, by a rule, `ReplaceRoundToWithQueryAndTags`, recently added, that propagates it, in some cases, to: - `LessThan` - `GreaterThanOrEqual` - `Range`
1 parent 944b9b6 commit 8172161

File tree

16 files changed

+153
-53
lines changed

16 files changed

+153
-53
lines changed

benchmarks/src/main/java/org/elasticsearch/benchmark/_nightly/esql/QueryPlanningBenchmark.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public void setup() {
7676
Locale.US,
7777
null,
7878
null,
79-
new QueryPragmas(Settings.EMPTY),
79+
QueryPragmas.EMPTY,
8080
AnalyzerSettings.QUERY_RESULT_TRUNCATION_MAX_SIZE.getDefault(Settings.EMPTY),
8181
AnalyzerSettings.QUERY_RESULT_TRUNCATION_DEFAULT_SIZE.getDefault(Settings.EMPTY),
8282
"",

benchmarks/src/main/java/org/elasticsearch/benchmark/compute/operator/EvalBenchmark.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.Equals;
5757
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThan;
5858
import org.elasticsearch.xpack.esql.planner.Layout;
59+
import org.elasticsearch.xpack.esql.plugin.QueryPragmas;
5960
import org.elasticsearch.xpack.esql.session.Configuration;
6061
import org.openjdk.jmh.annotations.Benchmark;
6162
import org.openjdk.jmh.annotations.BenchmarkMode;
@@ -358,7 +359,7 @@ private static Configuration configuration() {
358359
Locale.ROOT,
359360
null,
360361
null,
361-
null,
362+
QueryPragmas.EMPTY,
362363
AnalyzerSettings.QUERY_RESULT_TRUNCATION_MAX_SIZE.get(Settings.EMPTY),
363364
AnalyzerSettings.QUERY_RESULT_TRUNCATION_DEFAULT_SIZE.get(Settings.EMPTY),
364365
null,

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.xpack.esql.plugin.QueryPragmas;
2929

3030
import java.io.IOException;
31+
import java.time.ZoneId;
3132
import java.util.Iterator;
3233
import java.util.Locale;
3334
import java.util.Map;
@@ -47,6 +48,7 @@ public class EsqlQueryRequest extends org.elasticsearch.xpack.core.esql.action.E
4748
private boolean profile;
4849
private Boolean includeCCSMetadata;
4950
private Boolean includeExecutionMetadata;
51+
private ZoneId timeZone;
5052
private Locale locale;
5153
private QueryBuilder filter;
5254
private QueryPragmas pragmas = new QueryPragmas(Settings.EMPTY);
@@ -87,6 +89,12 @@ public ActionRequestValidationException validate() {
8789
}
8890

8991
if (onSnapshotBuild == false) {
92+
if (timeZone != null) {
93+
validationException = addValidationError(
94+
"[" + RequestXContent.TIME_ZONE_FIELD + "] only allowed in snapshot builds",
95+
validationException
96+
);
97+
}
9098
if (pragmas.isEmpty() == false && acceptedPragmaRisks == false) {
9199
validationException = addValidationError(
92100
"[" + RequestXContent.PRAGMA_FIELD + "] only allowed in snapshot builds",
@@ -158,6 +166,14 @@ public boolean profile() {
158166
return profile;
159167
}
160168

169+
public void timeZone(ZoneId timeZone) {
170+
this.timeZone = timeZone;
171+
}
172+
173+
public ZoneId timeZone() {
174+
return timeZone;
175+
}
176+
161177
public void locale(Locale locale) {
162178
this.locale = locale;
163179
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.elasticsearch.xpack.esql.plugin.QueryPragmas;
2323

2424
import java.io.IOException;
25+
import java.time.ZoneId;
2526
import java.util.ArrayList;
2627
import java.util.Arrays;
2728
import java.util.HashMap;
@@ -76,6 +77,7 @@ String fields() {
7677
private static final ParseField FILTER_FIELD = new ParseField("filter");
7778
static final ParseField PRAGMA_FIELD = new ParseField("pragma");
7879
private static final ParseField PARAMS_FIELD = new ParseField("params");
80+
static final ParseField TIME_ZONE_FIELD = new ParseField("time_zone");
7981
private static final ParseField LOCALE_FIELD = new ParseField("locale");
8082
private static final ParseField PROFILE_FIELD = new ParseField("profile");
8183
private static final ParseField ACCEPT_PRAGMA_RISKS = new ParseField("accept_pragma_risks");
@@ -113,6 +115,7 @@ private static void objectParserCommon(ObjectParser<EsqlQueryRequest, ?> parser)
113115
PRAGMA_FIELD
114116
);
115117
parser.declareField(EsqlQueryRequest::params, RequestXContent::parseParams, PARAMS_FIELD, VALUE_OBJECT_ARRAY);
118+
parser.declareString((request, timeZone) -> request.timeZone(ZoneId.of(timeZone)), TIME_ZONE_FIELD);
116119
parser.declareString((request, localeTag) -> request.locale(Locale.forLanguageTag(localeTag)), LOCALE_FIELD);
117120
parser.declareBoolean(EsqlQueryRequest::profile, PROFILE_FIELD);
118121
parser.declareField((p, r, c) -> new ParseTables(r, p).parseTables(), TABLES_FIELD, ObjectParser.ValueType.OBJECT);

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/EsqlStatement.java

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,71 @@
88
package org.elasticsearch.xpack.esql.plan;
99

1010
import org.elasticsearch.xpack.esql.core.expression.Expression;
11+
import org.elasticsearch.xpack.esql.core.expression.Literal;
1112
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
1213

1314
import java.util.List;
1415

1516
public record EsqlStatement(LogicalPlan plan, List<QuerySetting> settings) {
17+
/**
18+
* Returns the value of a setting, or the setting default value if the setting is not set.
19+
* If the setting name appears multiple times, this will return last occurrence.
20+
* <p>
21+
* Use it like:
22+
* </p>
23+
* <pre><code>
24+
* var value = statement.setting(QuerySettings.MY_SETTING);
25+
* </code></pre>
26+
*
27+
* @param settingDef the setting to retrieve
28+
*/
29+
public <T> T setting(QuerySettings.QuerySettingDef<T> settingDef) {
30+
return settingOrDefault(settingDef, settingDef.defaultValue());
31+
}
32+
33+
/**
34+
* Returns the value of a setting, but returns the given default value if the setting is not set.
35+
* <p>
36+
* Use it like:
37+
* </p>
38+
* <pre><code>
39+
* var value = statement.settingOrDefault(QuerySettings.MY_SETTING, "default");
40+
* </code></pre>
41+
* <p>
42+
* To be used when a fallback is available.
43+
* </p>
44+
*
45+
* @param settingDef the setting to retrieve
46+
* @param defaultValue the value to return if the setting is not set
47+
*/
48+
public <T> T settingOrDefault(QuerySettings.QuerySettingDef<T> settingDef, T defaultValue) {
49+
Expression expression = setting(settingDef.name());
50+
if (expression == null) {
51+
return defaultValue;
52+
}
53+
return settingDef.parse((Literal) expression);
54+
}
55+
1656
/**
1757
* Returns the expression corresponding to a setting value.
1858
* If the setting name appears multiple times, this will return last occurrence.
59+
* <p>
60+
* For testing purposes; use {@link #setting(QuerySettings.QuerySettingDef)} instead.
61+
* </p>
1962
*
2063
* @param name the setting name
2164
*/
2265
public Expression setting(String name) {
2366
if (settings == null) {
2467
return null;
2568
}
26-
Expression result = null;
69+
Expression expression = null;
2770
for (QuerySetting setting : settings) {
2871
if (setting.name().equals(name)) {
29-
result = setting.value();
72+
expression = setting.value();
3073
}
3174
}
32-
return result;
75+
return expression;
3376
}
3477

3578
@Override

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/QuerySettings.java

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import org.elasticsearch.core.Nullable;
1111
import org.elasticsearch.transport.RemoteClusterService;
12-
import org.elasticsearch.xpack.esql.core.expression.Expression;
12+
import org.elasticsearch.xpack.esql.core.expression.Literal;
1313
import org.elasticsearch.xpack.esql.core.type.DataType;
1414
import org.elasticsearch.xpack.esql.expression.Foldables;
1515
import org.elasticsearch.xpack.esql.parser.ParsingException;
@@ -34,8 +34,8 @@ public class QuerySettings {
3434
"A project routing expression, "
3535
+ "used to define which projects to route the query to. "
3636
+ "Only supported if Cross-Project Search is enabled.",
37-
(value, settings) -> Foldables.stringLiteralValueOf(value, "Unexpected value"),
38-
(_rcs) -> null
37+
(value) -> Foldables.stringLiteralValueOf(value, "Unexpected value"),
38+
null
3939
);
4040

4141
public static final QuerySettingDef<ZoneId> TIME_ZONE = new QuerySettingDef<>(
@@ -45,19 +45,19 @@ public class QuerySettings {
4545
true,
4646
true,
4747
"The default timezone to be used in the query, by the functions and commands that require it. Defaults to UTC",
48-
(value, _rcs) -> {
48+
(value) -> {
4949
String timeZone = Foldables.stringLiteralValueOf(value, "Unexpected value");
5050
try {
5151
return ZoneId.of(timeZone);
5252
} catch (Exception exc) {
5353
throw new IllegalArgumentException("Invalid time zone [" + timeZone + "]");
5454
}
5555
},
56-
(_rcs) -> ZoneOffset.UTC
56+
ZoneOffset.UTC
5757
);
5858

5959
public static final Map<String, QuerySettingDef<?>> SETTINGS_BY_NAME = Stream.of(PROJECT_ROUTING, TIME_ZONE)
60-
.collect(Collectors.toMap(QuerySettingDef::name, Function.identity()));;
60+
.collect(Collectors.toMap(QuerySettingDef::name, Function.identity()));
6161

6262
public static void validate(EsqlStatement statement, RemoteClusterService clusterService) {
6363
for (QuerySetting setting : statement.settings()) {
@@ -70,7 +70,14 @@ public static void validate(EsqlStatement statement, RemoteClusterService cluste
7070
throw new ParsingException(setting.source(), "Setting [" + setting.name() + "] must be of type " + def.type());
7171
}
7272

73-
String error = def.validator().validate(setting.value(), clusterService);
73+
Literal literal;
74+
if (setting.value() instanceof Literal l) {
75+
literal = l;
76+
} else {
77+
throw new ParsingException(setting.source(), "Setting [" + setting.name() + "] must have a literal value");
78+
}
79+
80+
String error = def.validator().validate(literal, clusterService);
7481
if (error != null) {
7582
throw new ParsingException("Error validating setting [" + setting.name() + "]: " + error);
7683
}
@@ -89,7 +96,7 @@ public static void validate(EsqlStatement statement, RemoteClusterService cluste
8996
* @param validator A validation function to check the setting value.
9097
* Defaults to calling the {@link #parser} and returning the error message of any exception it throws.
9198
* @param parser A function to parse the setting value into the final object.
92-
* @param defaultValueSupplier A supplier of the default value to be used when the setting is not set.
99+
* @param defaultValue A default value to be used when the setting is not set.
93100
* @param <T> The type of the setting value.
94101
*/
95102
public record QuerySettingDef<T>(
@@ -101,8 +108,11 @@ public record QuerySettingDef<T>(
101108
String description,
102109
Validator validator,
103110
Parser<T> parser,
104-
Function<RemoteClusterService, T> defaultValueSupplier
111+
T defaultValue
105112
) {
113+
/**
114+
* Constructor with a default validator that delegates to the parser.
115+
*/
106116
public QuerySettingDef(
107117
String name,
108118
DataType type,
@@ -111,23 +121,23 @@ public QuerySettingDef(
111121
boolean snapshotOnly,
112122
String description,
113123
Parser<T> parser,
114-
Function<RemoteClusterService, T> defaultValueSupplier
124+
T defaultValue
115125
) {
116126
this(name, type, serverlessOnly, preview, snapshotOnly, description, (value, rcs) -> {
117127
try {
118-
parser.parse(value, rcs);
128+
parser.parse(value);
119129
return null;
120130
} catch (Exception exc) {
121131
return exc.getMessage();
122132
}
123-
}, parser, defaultValueSupplier);
133+
}, parser, defaultValue);
124134
}
125135

126-
public T get(Expression value, RemoteClusterService clusterService) {
136+
public T parse(@Nullable Literal value) {
127137
if (value == null) {
128-
return defaultValueSupplier.apply(clusterService);
138+
return defaultValue;
129139
}
130-
return parser.parse(value, clusterService);
140+
return parser.parse(value);
131141
}
132142

133143
@FunctionalInterface
@@ -136,15 +146,15 @@ public interface Validator {
136146
* Validates the setting value and returns the error message if there's an error, or null otherwise.
137147
*/
138148
@Nullable
139-
String validate(Expression value, RemoteClusterService clusterService);
149+
String validate(Literal value, RemoteClusterService clusterService);
140150
}
141151

142152
@FunctionalInterface
143153
public interface Parser<T> {
144154
/**
145-
* Parses an already validated expression.
155+
* Parses an already validated literal.
146156
*/
147-
T parse(Expression value, RemoteClusterService clusterService);
157+
T parse(Literal value);
148158
}
149159
}
150160
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/Configuration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ public String toString() {
333333
+ ",timeseries="
334334
+ resultTruncationDefaultSize(true)
335335
+ "]"
336+
+ ", zoneId="
337+
+ zoneId
336338
+ ", locale="
337339
+ locale
338340
+ ", query='"

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@
8484
import org.elasticsearch.xpack.esql.plugin.TransportActionServices;
8585
import org.elasticsearch.xpack.esql.telemetry.PlanTelemetry;
8686

87-
import java.time.ZoneOffset;
8887
import java.util.ArrayList;
8988
import java.util.Collection;
9089
import java.util.HashMap;
@@ -184,7 +183,9 @@ public void execute(EsqlQueryRequest request, EsqlExecutionInfo executionInfo, P
184183
LOGGER.debug("ESQL query:\n{}", request.query());
185184
EsqlStatement statement = parse(request.query(), request.params());
186185
Configuration configuration = new Configuration(
187-
ZoneOffset.UTC, // TODO: Use the time_zone setting instead?
186+
request.timeZone() == null
187+
? statement.setting(QuerySettings.TIME_ZONE)
188+
: statement.settingOrDefault(QuerySettings.TIME_ZONE, request.timeZone()),
188189
request.locale() != null ? request.locale() : Locale.US,
189190
// TODO: plug-in security
190191
null,

0 commit comments

Comments
 (0)