Skip to content

Commit fda6992

Browse files
ES|QL: refactor query generator and move it to testFixtures (#134696)
1 parent 5e1bae4 commit fda6992

29 files changed

+424
-273
lines changed

x-pack/plugin/esql/qa/server/single-node/src/javaRestTest/java/org/elasticsearch/xpack/esql/qa/single_node/GenerativeMetricsIT.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
import org.elasticsearch.test.TestClustersThreadFilter;
1313
import org.elasticsearch.test.cluster.ElasticsearchCluster;
14-
import org.elasticsearch.xpack.esql.qa.rest.generative.EsqlQueryGenerator;
14+
import org.elasticsearch.xpack.esql.generator.EsqlQueryGenerator;
15+
import org.elasticsearch.xpack.esql.generator.command.CommandGenerator;
1516
import org.elasticsearch.xpack.esql.qa.rest.generative.GenerativeRestTest;
16-
import org.elasticsearch.xpack.esql.qa.rest.generative.command.CommandGenerator;
1717
import org.junit.ClassRule;
1818

1919
@ThreadLeakFilters(filters = TestClustersThreadFilter.class)

x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@
99

1010
import org.elasticsearch.client.Request;
1111
import org.elasticsearch.client.ResponseException;
12-
import org.elasticsearch.core.Nullable;
1312
import org.elasticsearch.test.rest.ESRestTestCase;
1413
import org.elasticsearch.xpack.esql.AssertWarnings;
1514
import org.elasticsearch.xpack.esql.CsvTestsDataLoader;
15+
import org.elasticsearch.xpack.esql.generator.Column;
16+
import org.elasticsearch.xpack.esql.generator.EsqlQueryGenerator;
17+
import org.elasticsearch.xpack.esql.generator.LookupIdx;
18+
import org.elasticsearch.xpack.esql.generator.LookupIdxColumn;
19+
import org.elasticsearch.xpack.esql.generator.QueryExecuted;
20+
import org.elasticsearch.xpack.esql.generator.QueryExecutor;
21+
import org.elasticsearch.xpack.esql.generator.command.CommandGenerator;
1622
import org.elasticsearch.xpack.esql.qa.rest.ProfileLogger;
1723
import org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase;
18-
import org.elasticsearch.xpack.esql.qa.rest.generative.command.CommandGenerator;
1924
import org.junit.AfterClass;
2025
import org.junit.Before;
2126
import org.junit.Rule;
@@ -32,11 +37,11 @@
3237
import static org.elasticsearch.xpack.esql.CsvTestsDataLoader.ENRICH_POLICIES;
3338
import static org.elasticsearch.xpack.esql.CsvTestsDataLoader.availableDatasetsForEs;
3439
import static org.elasticsearch.xpack.esql.CsvTestsDataLoader.loadDataSetIntoEs;
35-
import static org.elasticsearch.xpack.esql.qa.rest.generative.EsqlQueryGenerator.COLUMN_NAME;
36-
import static org.elasticsearch.xpack.esql.qa.rest.generative.EsqlQueryGenerator.COLUMN_ORIGINAL_TYPES;
37-
import static org.elasticsearch.xpack.esql.qa.rest.generative.EsqlQueryGenerator.COLUMN_TYPE;
40+
import static org.elasticsearch.xpack.esql.generator.EsqlQueryGenerator.COLUMN_NAME;
41+
import static org.elasticsearch.xpack.esql.generator.EsqlQueryGenerator.COLUMN_ORIGINAL_TYPES;
42+
import static org.elasticsearch.xpack.esql.generator.EsqlQueryGenerator.COLUMN_TYPE;
3843

39-
public abstract class GenerativeRestTest extends ESRestTestCase {
44+
public abstract class GenerativeRestTest extends ESRestTestCase implements QueryExecutor {
4045

4146
@Rule(order = Integer.MIN_VALUE)
4247
public ProfileLogger profileLogger = new ProfileLogger();
@@ -56,7 +61,8 @@ public abstract class GenerativeRestTest extends ESRestTestCase {
5661

5762
// Awaiting fixes for query failure
5863
"Unknown column \\[<all-fields-projected>\\]", // https://github.com/elastic/elasticsearch/issues/121741,
59-
"Plan \\[ProjectExec\\[\\[<no-fields>.* optimized incorrectly due to missing references", // https://github.com/elastic/elasticsearch/issues/125866
64+
// https://github.com/elastic/elasticsearch/issues/125866
65+
"Plan \\[ProjectExec\\[\\[<no-fields>.* optimized incorrectly due to missing references",
6066
"The incoming YAML document exceeds the limit:", // still to investigate, but it seems to be specific to the test framework
6167
"Data too large", // Circuit breaker exceptions eg. https://github.com/elastic/elasticsearch/issues/130072
6268
"optimized incorrectly due to missing references", // https://github.com/elastic/elasticsearch/issues/131509
@@ -69,7 +75,8 @@ public abstract class GenerativeRestTest extends ESRestTestCase {
6975
"count_star .* can't be used with TS command",
7076
"time_series aggregate.* can only be used with the TS command",
7177
"Invalid call to dataType on an unresolved object \\?LASTOVERTIME", // https://github.com/elastic/elasticsearch/issues/134791
72-
"class org.elasticsearch.compute.data..*Block cannot be cast to class org.elasticsearch.compute.data..*Block", // https://github.com/elastic/elasticsearch/issues/134793
78+
// https://github.com/elastic/elasticsearch/issues/134793
79+
"class org.elasticsearch.compute.data..*Block cannot be cast to class org.elasticsearch.compute.data..*Block",
7380
"Output has changed from \\[.*\\] to \\[.*\\]" // https://github.com/elastic/elasticsearch/issues/134794
7481
);
7582

@@ -116,9 +123,9 @@ public void run(CommandGenerator generator, CommandGenerator.CommandDescription
116123
previousCommands.add(current);
117124
final String command = current.commandString();
118125

119-
final EsqlQueryGenerator.QueryExecuted result = previousResult == null
120-
? execute(command, 0, profileLogger)
121-
: execute(previousResult.query() + command, previousResult.depth(), profileLogger);
126+
final QueryExecuted result = previousResult == null
127+
? execute(command, 0)
128+
: execute(previousResult.query() + command, previousResult.depth());
122129
previousResult = result;
123130

124131
final boolean hasException = result.exception() != null;
@@ -145,16 +152,16 @@ public boolean continueExecuting() {
145152
}
146153

147154
@Override
148-
public List<EsqlQueryGenerator.Column> currentSchema() {
155+
public List<Column> currentSchema() {
149156
return currentSchema;
150157
}
151158

152159
boolean continueExecuting;
153-
List<EsqlQueryGenerator.Column> currentSchema;
160+
List<Column> currentSchema;
154161
final List<CommandGenerator.CommandDescription> previousCommands = new ArrayList<>();
155-
EsqlQueryGenerator.QueryExecuted previousResult;
162+
QueryExecuted previousResult;
156163
};
157-
EsqlQueryGenerator.generatePipeline(MAX_DEPTH, sourceCommand(), mappingInfo, exec, requiresTimeSeries());
164+
EsqlQueryGenerator.generatePipeline(MAX_DEPTH, sourceCommand(), mappingInfo, exec, requiresTimeSeries(), this);
158165
}
159166
}
160167

@@ -166,8 +173,8 @@ private static CommandGenerator.ValidationResult checkResults(
166173
List<CommandGenerator.CommandDescription> previousCommands,
167174
CommandGenerator commandGenerator,
168175
CommandGenerator.CommandDescription commandDescription,
169-
EsqlQueryGenerator.QueryExecuted previousResult,
170-
EsqlQueryGenerator.QueryExecuted result
176+
QueryExecuted previousResult,
177+
QueryExecuted result
171178
) {
172179
CommandGenerator.ValidationResult outputValidation = commandGenerator.validateOutput(
173180
previousCommands,
@@ -188,7 +195,7 @@ private static CommandGenerator.ValidationResult checkResults(
188195
return outputValidation;
189196
}
190197

191-
private void checkException(EsqlQueryGenerator.QueryExecuted query) {
198+
private void checkException(QueryExecuted query) {
192199
for (Pattern allowedError : ALLOWED_ERROR_PATTERNS) {
193200
if (isAllowedError(query.exception().getMessage(), allowedError)) {
194201
return;
@@ -208,35 +215,36 @@ private static boolean isAllowedError(String errorMessage, Pattern allowedPatter
208215
return allowedPattern.matcher(errorWithoutLineBreaks).matches();
209216
}
210217

218+
@Override
211219
@SuppressWarnings("unchecked")
212-
public static EsqlQueryGenerator.QueryExecuted execute(String command, int depth, @Nullable ProfileLogger profileLogger) {
220+
public QueryExecuted execute(String query, int depth) {
213221
try {
214222
Map<String, Object> json = RestEsqlTestCase.runEsql(
215-
new RestEsqlTestCase.RequestObjectBuilder().query(command).build(),
223+
new RestEsqlTestCase.RequestObjectBuilder().query(query).build(),
216224
new AssertWarnings.AllowedRegexes(List.of(Pattern.compile(".*"))),// we don't care about warnings
217225
profileLogger,
218226
RestEsqlTestCase.Mode.SYNC
219227
);
220-
List<EsqlQueryGenerator.Column> outputSchema = outputSchema(json);
228+
List<Column> outputSchema = outputSchema(json);
221229
List<List<Object>> values = (List<List<Object>>) json.get("values");
222-
return new EsqlQueryGenerator.QueryExecuted(command, depth, outputSchema, values, null);
230+
return new QueryExecuted(query, depth, outputSchema, values, null);
223231
} catch (Exception e) {
224-
return new EsqlQueryGenerator.QueryExecuted(command, depth, null, null, e);
232+
return new QueryExecuted(query, depth, null, null, e);
225233
} catch (AssertionError ae) {
226234
// this is for ensureNoWarnings()
227-
return new EsqlQueryGenerator.QueryExecuted(command, depth, null, null, new RuntimeException(ae.getMessage()));
235+
return new QueryExecuted(query, depth, null, null, new RuntimeException(ae.getMessage()));
228236
}
229237

230238
}
231239

232240
@SuppressWarnings("unchecked")
233-
private static List<EsqlQueryGenerator.Column> outputSchema(Map<String, Object> a) {
241+
private static List<Column> outputSchema(Map<String, Object> a) {
234242
List<Map<String, ?>> cols = (List<Map<String, ?>>) a.get("columns");
235243
if (cols == null) {
236244
return null;
237245
}
238246
return cols.stream()
239-
.map(x -> new EsqlQueryGenerator.Column((String) x.get(COLUMN_NAME), (String) x.get(COLUMN_TYPE), originalTypes(x)))
247+
.map(x -> new Column((String) x.get(COLUMN_NAME), (String) x.get(COLUMN_TYPE), originalTypes(x)))
240248
.collect(Collectors.toList());
241249
}
242250

@@ -256,10 +264,6 @@ private List<String> availableIndices() throws IOException {
256264
.toList();
257265
}
258266

259-
public record LookupIdxColumn(String name, String type) {}
260-
261-
public record LookupIdx(String idxName, List<LookupIdxColumn> keys) {}
262-
263267
private List<LookupIdx> lookupIndices() {
264268
List<LookupIdx> result = new ArrayList<>();
265269
// we don't have key info from the dataset loader, let's hardcode it for now

x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/README.asciidoc

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,11 @@ The result check happens at two levels:
2626
2727
== Implementing your own command generator
2828

29-
If you implement a new command, and you want it to be tested by the generative tests, you can add a command generator here.
29+
If you create a new command, we strongly recommend you to implement a command generator for it, so that it is tested
30+
by the generative tests.
3031

31-
All you have to do is:
32+
To do so, see the documentation in the
33+
`x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/generator/README.asciidoc` file.
3234

33-
* add a class in `org.elasticsearch.xpack.esql.qa.rest.generative.command.source` (if it's a source command) or in `org.elasticsearch.xpack.esql.qa.rest.generative.command.pipe` (if it's a pipe command)
34-
* Implement `CommandGenerator` interface (see its javadoc, it should be explicative. Or just have a look at one of the existing commands, eg. `SortGenerator`)
35-
** Implement `CommandGenerator.generate()` method, that will return the command.
36-
*** Have a look at `EsqlQueryGenerator`, it contains many utility methods that will help you generate random expressions.
37-
** Implement `CommandGenerator.validateOutput()` to validate the output of the query.
38-
* Add your class to `EsqlQueryGenerator.SOURCE_COMMANDS` (if it's a source command) or `EsqlQueryGenerator.PIPE_COMMANDS` (if it's a pipe command).
39-
* Run `GenerativeIT` at least a couple of times: these tests can be pretty noisy.
40-
* If you get unexpected errors (real bugs in ES|QL), please open an issue and add the error to `GenerativeRestTest.ALLOWED_ERRORS`. Run tests again until everything works fine.
4135

4236

43-
IMPORTANT: be careful when validating the output (Eg. the row count), as ES|QL can be quite non-deterministic when there are no SORTs
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql.generator;
9+
10+
import java.util.List;
11+
12+
/**
13+
* A column in the output schema of a query execution.
14+
* @param name the field name
15+
* @param type the field type
16+
* @param originalTypes the original types, in case of a union type.
17+
*/
18+
public record Column(String name, String type, List<String> originalTypes) {}

0 commit comments

Comments
 (0)