Skip to content

Commit 2d627e7

Browse files
committed
ESQL: Added time_zone settings and added parsing to settings
1 parent 50a2c72 commit 2d627e7

File tree

4 files changed

+163
-93
lines changed

4 files changed

+163
-93
lines changed

docs/reference/query-languages/esql/kibana/definition/settings/time_zone.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 99 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,96 +7,63 @@
77

88
package org.elasticsearch.xpack.esql.plan;
99

10+
import org.elasticsearch.core.Nullable;
1011
import org.elasticsearch.transport.RemoteClusterService;
12+
import org.elasticsearch.xpack.esql.core.QlIllegalArgumentException;
13+
import org.elasticsearch.xpack.esql.core.expression.Expression;
1114
import org.elasticsearch.xpack.esql.core.type.DataType;
15+
import org.elasticsearch.xpack.esql.expression.Foldables;
1216
import org.elasticsearch.xpack.esql.parser.ParsingException;
1317

14-
import java.util.function.Predicate;
18+
import java.time.ZoneId;
1519

16-
public enum QuerySettings {
20+
public class QuerySettings {
1721
// TODO check cluster state and see if project routing is allowed
1822
// see https://github.com/elastic/elasticsearch/pull/134446
1923
// PROJECT_ROUTING(..., state -> state.getRemoteClusterNames().crossProjectEnabled());
20-
PROJECT_ROUTING(
24+
public static final QuerySettingDef<String> PROJECT_ROUTING = new QuerySettingDef<>(
2125
"project_routing",
2226
DataType.KEYWORD,
2327
true,
2428
false,
2529
true,
2630
"A project routing expression, "
2731
+ "used to define which projects to route the query to. "
28-
+ "Only supported if Cross-Project Search is enabled."
29-
),;
32+
+ "Only supported if Cross-Project Search is enabled.",
33+
(value, settings) -> Foldables.stringLiteralValueOf(value, "Unexpected value")
34+
);
3035

31-
private String settingName;
32-
private DataType type;
33-
private final boolean serverlessOnly;
34-
private final boolean snapshotOnly;
35-
private final boolean preview;
36-
private final String description;
37-
private final Predicate<RemoteClusterService> validator;
38-
39-
QuerySettings(
40-
String name,
41-
DataType type,
42-
boolean serverlessOnly,
43-
boolean preview,
44-
boolean snapshotOnly,
45-
String description,
46-
Predicate<RemoteClusterService> validator
47-
) {
48-
this.settingName = name;
49-
this.type = type;
50-
this.serverlessOnly = serverlessOnly;
51-
this.preview = preview;
52-
this.snapshotOnly = snapshotOnly;
53-
this.description = description;
54-
this.validator = validator;
55-
}
56-
57-
QuerySettings(String name, DataType type, boolean serverlessOnly, boolean preview, boolean snapshotOnly, String description) {
58-
this(name, type, serverlessOnly, preview, snapshotOnly, description, state -> true);
59-
}
60-
61-
public String settingName() {
62-
return settingName;
63-
}
64-
65-
public DataType type() {
66-
return type;
67-
}
68-
69-
public boolean serverlessOnly() {
70-
return serverlessOnly;
71-
}
72-
73-
public boolean snapshotOnly() {
74-
return snapshotOnly;
75-
}
76-
77-
public boolean preview() {
78-
return preview;
79-
}
80-
81-
public String description() {
82-
return description;
83-
}
36+
public static final QuerySettingDef<ZoneId> TIME_ZONE = new QuerySettingDef<>(
37+
"time_zone",
38+
DataType.KEYWORD,
39+
false,
40+
true,
41+
true,
42+
"The default timezone to be used in the query, by the functions and commands that require it. Defaults to UTC",
43+
(value, _rcs) -> {
44+
String timeZone = Foldables.stringLiteralValueOf(value, "Unexpected value");
45+
try {
46+
return ZoneId.of(timeZone);
47+
} catch (Exception exc) {
48+
throw new QlIllegalArgumentException("Invalid time zone [" + timeZone + "]");
49+
}
50+
}
51+
);
8452

85-
public Predicate<RemoteClusterService> validator() {
86-
return validator;
87-
}
53+
public static final QuerySettingDef<?>[] ALL_SETTINGS = { PROJECT_ROUTING, TIME_ZONE };
8854

8955
public static void validate(EsqlStatement statement, RemoteClusterService clusterService) {
9056
for (QuerySetting setting : statement.settings()) {
9157
boolean found = false;
92-
for (QuerySettings qs : values()) {
93-
if (qs.settingName().equals(setting.name())) {
58+
for (QuerySettingDef<?> def : ALL_SETTINGS) {
59+
if (def.name().equals(setting.name())) {
9460
found = true;
95-
if (setting.value().dataType() != qs.type()) {
96-
throw new ParsingException(setting.source(), "Setting [" + setting.name() + "] must be of type " + qs.type());
61+
if (setting.value().dataType() != def.type()) {
62+
throw new ParsingException(setting.source(), "Setting [" + setting.name() + "] must be of type " + def.type());
9763
}
98-
if (qs.validator().test(clusterService) == false) {
99-
throw new ParsingException(setting.source(), "Setting [" + setting.name() + "] is not allowed");
64+
String error = def.validator().validate(setting.value(), clusterService);
65+
if (error != null) {
66+
throw new ParsingException("Error validating setting [" + setting.name() + "]: " + error);
10067
}
10168
break;
10269
}
@@ -106,4 +73,69 @@ public static void validate(EsqlStatement statement, RemoteClusterService cluste
10673
}
10774
}
10875
}
76+
77+
/**
78+
* Definition of a query setting.
79+
*
80+
* @param name The name to be used when setting it in the query. E.g. {@code SET name=value}
81+
* @param type The allowed datatype of the setting.
82+
* @param serverlessOnly
83+
* @param preview
84+
* @param snapshotOnly
85+
* @param description The user-facing description of the setting.
86+
* @param validator A validation function to check the setting value.
87+
* Defaults to calling the {@link #parser} and returning the error message of any exception it throws.
88+
* @param parser A function to parse the setting value into the final object.
89+
* @param <T> The type of the setting value.
90+
*/
91+
public record QuerySettingDef<T>(
92+
String name,
93+
DataType type,
94+
boolean serverlessOnly,
95+
boolean preview,
96+
boolean snapshotOnly,
97+
String description,
98+
Validator validator,
99+
Parser<T> parser
100+
) {
101+
public QuerySettingDef(
102+
String name,
103+
DataType type,
104+
boolean serverlessOnly,
105+
boolean preview,
106+
boolean snapshotOnly,
107+
String description,
108+
Parser<T> parser
109+
) {
110+
this(name, type, serverlessOnly, preview, snapshotOnly, description, (value, rcs) -> {
111+
try {
112+
parser.parse(value, rcs);
113+
return null;
114+
} catch (Exception exc) {
115+
return exc.getMessage();
116+
}
117+
}, parser);
118+
}
119+
120+
public T get(Expression value, RemoteClusterService clusterService) {
121+
return parser.parse(value, clusterService);
122+
}
123+
124+
@FunctionalInterface
125+
public interface Validator {
126+
/**
127+
* Validates the setting value and returns the error message if there's an error, or null otherwise.
128+
*/
129+
@Nullable
130+
String validate(Expression value, RemoteClusterService clusterService);
131+
}
132+
133+
@FunctionalInterface
134+
public interface Parser<T> {
135+
/**
136+
* Parses an already validated expression.
137+
*/
138+
T parse(Expression value, RemoteClusterService clusterService);
139+
}
140+
}
109141
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/DocsV3Support.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThan;
4343
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.LessThanOrEqual;
4444
import org.elasticsearch.xpack.esql.expression.predicate.operator.comparison.NotEquals;
45-
import org.elasticsearch.xpack.esql.plan.QuerySettings;
45+
import org.elasticsearch.xpack.esql.plan.QuerySettings.QuerySettingDef;
4646
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
4747
import org.elasticsearch.xpack.esql.session.Configuration;
4848

@@ -1081,10 +1081,10 @@ void renderTypes(String name, List<EsqlFunctionRegistry.ArgSignature> args) thro
10811081

10821082
public static class SettingsDocsSupport extends DocsV3Support {
10831083

1084-
private final QuerySettings setting;
1084+
private final QuerySettingDef<?> setting;
10851085

1086-
public SettingsDocsSupport(QuerySettings setting, Class<?> testClass, Callbacks callbacks) {
1087-
super("settings", setting.settingName(), testClass, Set::of, callbacks);
1086+
public SettingsDocsSupport(QuerySettingDef<?> setting, Class<?> testClass, Callbacks callbacks) {
1087+
super("settings", setting.name(), testClass, Set::of, callbacks);
10881088
this.setting = setting;
10891089
}
10901090

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/QuerySettingsTests.java

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,56 +7,85 @@
77

88
package org.elasticsearch.xpack.esql.plan;
99

10-
import org.elasticsearch.common.lucene.BytesRefs;
1110
import org.elasticsearch.test.ESTestCase;
1211
import org.elasticsearch.xpack.esql.core.expression.Alias;
12+
import org.elasticsearch.xpack.esql.core.expression.Expression;
1313
import org.elasticsearch.xpack.esql.core.expression.Literal;
1414
import org.elasticsearch.xpack.esql.core.tree.Source;
1515
import org.elasticsearch.xpack.esql.core.type.DataType;
1616
import org.elasticsearch.xpack.esql.expression.function.DocsV3Support;
1717
import org.elasticsearch.xpack.esql.parser.ParsingException;
18+
import org.hamcrest.Matcher;
1819
import org.junit.AfterClass;
1920

21+
import java.time.ZoneId;
2022
import java.util.List;
2123

2224
import static org.hamcrest.Matchers.containsString;
25+
import static org.hamcrest.Matchers.equalTo;
2326

2427
public class QuerySettingsTests extends ESTestCase {
28+
public void testNonExistingSetting() {
29+
String settingName = "non_existing";
2530

26-
public void test() {
31+
assertInvalid(settingName, Literal.keyword(Source.EMPTY, "12"), "Unknown setting [" + settingName + "]");
32+
}
2733

28-
QuerySetting project_routing = new QuerySetting(
29-
Source.EMPTY,
30-
new Alias(Source.EMPTY, "project_routing", new Literal(Source.EMPTY, BytesRefs.toBytesRef("my-project"), DataType.KEYWORD))
31-
);
32-
QuerySettings.validate(new EsqlStatement(null, List.of(project_routing)), null);
34+
public void testProjectRouting() {
35+
var setting = QuerySettings.PROJECT_ROUTING;
3336

34-
QuerySetting wrong_type = new QuerySetting(
35-
Source.EMPTY,
36-
new Alias(Source.EMPTY, "project_routing", new Literal(Source.EMPTY, 12, DataType.INTEGER))
37-
);
38-
assertThat(
39-
expectThrows(ParsingException.class, () -> QuerySettings.validate(new EsqlStatement(null, List.of(wrong_type)), null))
40-
.getMessage(),
41-
containsString("Setting [project_routing] must be of type KEYWORD")
37+
assertValid(setting, Literal.keyword(Source.EMPTY, "my-project"), equalTo("my-project"));
38+
39+
assertInvalid(
40+
setting.name(),
41+
new Literal(Source.EMPTY, 12, DataType.INTEGER),
42+
"Setting [" + setting.name() + "] must be of type KEYWORD"
4243
);
44+
}
45+
46+
public void testTimeZone() {
47+
var setting = QuerySettings.TIME_ZONE;
4348

44-
QuerySetting non_existing = new QuerySetting(
45-
Source.EMPTY,
46-
new Alias(Source.EMPTY, "non_existing", new Literal(Source.EMPTY, BytesRefs.toBytesRef("12"), DataType.KEYWORD))
49+
assertValid(setting, Literal.keyword(Source.EMPTY, "Europe/Madrid"), equalTo(ZoneId.of("Europe/Madrid")));
50+
assertValid(setting, Literal.keyword(Source.EMPTY, "+05:00"), equalTo(ZoneId.of("+05:00")));
51+
assertValid(setting, Literal.keyword(Source.EMPTY, "+05"), equalTo(ZoneId.of("+05")));
52+
assertValid(setting, Literal.keyword(Source.EMPTY, "+07:15"), equalTo(ZoneId.of("+07:15")));
53+
54+
assertInvalid(setting.name(), Literal.integer(Source.EMPTY, 12), "Setting [" + setting.name() + "] must be of type KEYWORD");
55+
assertInvalid(
56+
setting.name(),
57+
Literal.keyword(Source.EMPTY, "Europe/New York"),
58+
"Error validating setting [" + setting.name() + "]: Invalid time zone [Europe/New York]"
4759
);
60+
}
61+
62+
private static <T> void assertValid(
63+
QuerySettings.QuerySettingDef<T> settingDef,
64+
Expression valueExpression,
65+
Matcher<T> parsedValueMatcher
66+
) {
67+
QuerySetting setting = new QuerySetting(Source.EMPTY, new Alias(Source.EMPTY, settingDef.name(), valueExpression));
68+
QuerySettings.validate(new EsqlStatement(null, List.of(setting)), null);
69+
70+
T value = settingDef.get(valueExpression, null);
71+
72+
assertThat(value, parsedValueMatcher);
73+
}
74+
75+
private static void assertInvalid(String settingName, Expression valueExpression, String expectedMessage) {
76+
QuerySetting setting = new QuerySetting(Source.EMPTY, new Alias(Source.EMPTY, settingName, valueExpression));
4877
assertThat(
49-
expectThrows(ParsingException.class, () -> QuerySettings.validate(new EsqlStatement(null, List.of(non_existing)), null))
78+
expectThrows(ParsingException.class, () -> QuerySettings.validate(new EsqlStatement(null, List.of(setting)), null))
5079
.getMessage(),
51-
containsString("Unknown setting [non_existing]")
80+
containsString(expectedMessage)
5281
);
5382
}
5483

5584
@AfterClass
5685
public static void generateDocs() throws Exception {
57-
for (QuerySettings value : QuerySettings.values()) {
86+
for (QuerySettings.QuerySettingDef<?> def : QuerySettings.ALL_SETTINGS) {
5887
DocsV3Support.SettingsDocsSupport settingsDocsSupport = new DocsV3Support.SettingsDocsSupport(
59-
value,
88+
def,
6089
QuerySettingsTests.class,
6190
DocsV3Support.callbacksFromSystemProperty()
6291
);

0 commit comments

Comments
 (0)