diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/EsqlQueryGenerator.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/EsqlQueryGenerator.java index cb0fe098dbf67..adda509ee12b9 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/EsqlQueryGenerator.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/EsqlQueryGenerator.java @@ -9,6 +9,7 @@ import org.elasticsearch.xpack.esql.CsvTestsDataLoader; import org.elasticsearch.xpack.esql.qa.rest.generative.command.CommandGenerator; +import org.elasticsearch.xpack.esql.qa.rest.generative.command.pipe.ChangePointGenerator; import org.elasticsearch.xpack.esql.qa.rest.generative.command.pipe.DissectGenerator; import org.elasticsearch.xpack.esql.qa.rest.generative.command.pipe.DropGenerator; import org.elasticsearch.xpack.esql.qa.rest.generative.command.pipe.EnrichGenerator; @@ -49,6 +50,7 @@ public record QueryExecuted(String query, int depth, List outputSchema, * These are downstream commands, ie. that cannot appear as the first command in a query */ static List PIPE_COMMANDS = List.of( + ChangePointGenerator.INSTANCE, DissectGenerator.INSTANCE, DropGenerator.INSTANCE, EnrichGenerator.INSTANCE, @@ -197,6 +199,10 @@ public static String randomNumericOrDateField(List previousOutput) { return randomName(previousOutput, Set.of("long", "integer", "double", "date")); } + public static String randomDateField(List previousOutput) { + return randomName(previousOutput, Set.of("date")); + } + public static String randomNumericField(List previousOutput) { return randomName(previousOutput, Set.of("long", "integer", "double")); } @@ -255,6 +261,22 @@ public static String constantExpression() { } + /** + * returns a random identifier or one of the existing names + */ + public static String randomAttributeOrIdentifier(List previousOutput) { + String name; + if (randomBoolean()) { + name = EsqlQueryGenerator.randomIdentifier(); + } else { + name = EsqlQueryGenerator.randomName(previousOutput); + if (name == null) { + name = EsqlQueryGenerator.randomIdentifier(); + } + } + return name; + } + public static String randomIdentifier() { // Let's create identifiers that are long enough to avoid collisions with reserved keywords. // There could be a smarter way (introspection on the lexer class?), but probably it's not worth the effort diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java index 21d0bf2ba49f8..d2c887576df94 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java @@ -10,6 +10,7 @@ import org.elasticsearch.client.Request; import org.elasticsearch.client.ResponseException; import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xpack.esql.AssertWarnings; import org.elasticsearch.xpack.esql.CsvTestsDataLoader; import org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase; import org.elasticsearch.xpack.esql.qa.rest.generative.command.CommandGenerator; @@ -44,10 +45,6 @@ public abstract class GenerativeRestTest extends ESRestTestCase { "The field names are too complex to process", // field_caps problem "must be \\[any type except counter types\\]", // TODO refine the generation of count() - // warnings - "Field '.*' shadowed by field at line .*", - "evaluation of \\[.*\\] failed, treating result as null", // TODO investigate? - // Awaiting fixes for query failure "Unknown column \\[\\]", // https://github.com/elastic/elasticsearch/issues/121741, "Plan \\[ProjectExec\\[\\[.* optimized incorrectly due to missing references", // https://github.com/elastic/elasticsearch/issues/125866 @@ -99,10 +96,10 @@ public void test() throws IOException { EsqlQueryGenerator.QueryExecuted result = execute(command, 0); if (result.exception() != null) { checkException(result); - break; + continue; } if (checkResults(List.of(), commandGenerator, desc, null, result).success() == false) { - break; + continue; } previousResult = result; previousCommands.add(desc); @@ -168,7 +165,11 @@ private void checkException(EsqlQueryGenerator.QueryExecuted query) { @SuppressWarnings("unchecked") private EsqlQueryGenerator.QueryExecuted execute(String command, int depth) { try { - Map a = RestEsqlTestCase.runEsqlSync(new RestEsqlTestCase.RequestObjectBuilder().query(command).build()); + Map a = RestEsqlTestCase.runEsql( + new RestEsqlTestCase.RequestObjectBuilder().query(command).build(), + new AssertWarnings.AllowedRegexes(List.of(Pattern.compile(".*"))),// we don't care about warnings + RestEsqlTestCase.Mode.SYNC + ); List outputSchema = outputSchema(a); List> values = (List>) a.get("values"); return new EsqlQueryGenerator.QueryExecuted(command, depth, outputSchema, values, null); diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/command/CommandGenerator.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/command/CommandGenerator.java index 7d652fddbc87a..7d22ea4c66a94 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/command/CommandGenerator.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/command/CommandGenerator.java @@ -139,4 +139,22 @@ static ValidationResult expectSameColumns(List previo return VALIDATION_OK; } + + /** + * The command doesn't have to produce LESS columns than the previous query + */ + static ValidationResult expectAtLeastSameNumberOfColumns( + List previousColumns, + List columns + ) { + if (previousColumns.stream().anyMatch(x -> x.name().contains(""))) { + return VALIDATION_OK; // known bug + } + + if (previousColumns.size() > columns.size()) { + return new ValidationResult(false, "Expecting at least [" + previousColumns.size() + "] columns, got [" + columns.size() + "]"); + } + + return VALIDATION_OK; + } } diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/command/pipe/ChangePointGenerator.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/command/pipe/ChangePointGenerator.java new file mode 100644 index 0000000000000..a0d71c47d413b --- /dev/null +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/command/pipe/ChangePointGenerator.java @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.esql.qa.rest.generative.command.pipe; + +import org.elasticsearch.xpack.esql.qa.rest.generative.EsqlQueryGenerator; +import org.elasticsearch.xpack.esql.qa.rest.generative.command.CommandGenerator; + +import java.util.List; +import java.util.Map; + +import static org.elasticsearch.xpack.esql.qa.rest.generative.EsqlQueryGenerator.randomAttributeOrIdentifier; + +public class ChangePointGenerator implements CommandGenerator { + public static final String CHANGE_POINT = "change_point"; + public static final CommandGenerator INSTANCE = new ChangePointGenerator(); + + @Override + public CommandDescription generate( + List previousCommands, + List previousOutput, + QuerySchema schema + ) { + String timestampField = EsqlQueryGenerator.randomDateField(previousOutput); + String numericField = EsqlQueryGenerator.randomNumericField(previousOutput); + if (timestampField == null || numericField == null) { + return EMPTY_DESCRIPTION; + } + String alias1 = randomAttributeOrIdentifier(previousOutput); + String alias2 = randomAttributeOrIdentifier(previousOutput); + while (alias1.equals(alias2)) { + alias2 = randomAttributeOrIdentifier(previousOutput); + } + + String cmd = " | CHANGE_POINT " + numericField + " ON " + timestampField + " AS " + alias1 + ", " + alias2; + + return new CommandDescription(CHANGE_POINT, this, cmd, Map.of()); + } + + @Override + public ValidationResult validateOutput( + List previousCommands, + CommandDescription command, + List previousColumns, + List> previousOutput, + List columns, + List> output + ) { + return CommandGenerator.expectAtLeastSameNumberOfColumns(previousColumns, columns); + } +}