Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@
import org.elasticsearch.xpack.esql.generator.QueryExecuted;
import org.elasticsearch.xpack.esql.generator.QueryExecutor;
import org.elasticsearch.xpack.esql.generator.command.CommandGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.DissectGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.EnrichGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.EvalGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.GrokGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.InlineStatsGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.LookupJoinGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.MvExpandGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.RegisteredDomainGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.RenameGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.StatsGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.UriPartsGenerator;
import org.elasticsearch.xpack.esql.generator.command.source.FromGenerator;
import org.elasticsearch.xpack.esql.qa.rest.ProfileLogger;
import org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase;
Expand Down Expand Up @@ -588,19 +596,19 @@ static List<Column> updateIndexMapped(
Set<String> createdColumns = new HashSet<>();

switch (commandName) {
case "eval" -> {
case EvalGenerator.EVAL -> {
Object newCols = command.context().get(EvalGenerator.NEW_COLUMNS);
if (newCols instanceof List<?> list) {
list.forEach(name -> createdColumns.add((String) name));
}
}
case "grok" -> {
case GrokGenerator.GROK -> {
Matcher gm = GROK_GENERATED_FIELD_PATTERN.matcher(command.commandString());
while (gm.find()) {
createdColumns.add(unquote(gm.group(1)));
}
}
case "dissect" -> {
case DissectGenerator.DISSECT -> {
Matcher dm = DISSECT_GENERATED_FIELD_PATTERN.matcher(command.commandString());
while (dm.find()) {
String generated = dm.group(1);
Expand All @@ -609,27 +617,27 @@ static List<Column> updateIndexMapped(
}
}
}
case "mv_expand" -> {
case MvExpandGenerator.MV_EXPAND -> {
String expanded = command.commandString().replaceFirst("(?i)^\\s*\\|\\s*mv_expand\\s+", "").trim();
// Not truly a newly created column, but we need to override the indexMapped flag so that full-text functions don't use it.
// https://github.com/elastic/elasticsearch/issues/142713
createdColumns.add(unquote(expanded));
}
case "stats", "inline stats" -> {
case StatsGenerator.STATS, InlineStatsGenerator.INLINE_STATS -> {
return newSchema.stream().map(col -> new Column(col.name(), col.type(), col.originalTypes(), false)).toList();
}
case "rename" -> {
case RenameGenerator.RENAME -> {
return handleRenameIndexMapped(newSchema, prevMapped, command.commandString());
}
case "registered_domain" -> {
case RegisteredDomainGenerator.REGISTERED_DOMAIN -> {
String prefix = (String) command.context().get("prefix");
if (prefix != null) {
for (String subField : List.of("domain", "registered_domain", "top_level_domain", "subdomain")) {
createdColumns.add(prefix + "." + subField);
}
}
}
case "uri_parts" -> {
case UriPartsGenerator.URI_PARTS -> {
String prefix = (String) command.context().get("prefix");
if (prefix != null) {
for (Column col : newSchema) {
Expand All @@ -639,15 +647,15 @@ static List<Column> updateIndexMapped(
}
}
}
case "enrich" -> {
case EnrichGenerator.ENRICH -> {
// Enrich fields can shadow existing index columns, so we use the policy's declared enrich_fields
// from the context to ensure they are marked as non-index-mapped even when names collide.
Object enrichFieldsObj = command.context().get(EnrichGenerator.ENRICH_FIELDS);
if (enrichFieldsObj instanceof List<?> enrichFieldsList) {
enrichFieldsList.forEach(name -> createdColumns.add((String) name));
}
}
case "lookup join" -> {
case LookupJoinGenerator.LOOKUP_JOIN -> {
// LookupJoinGenerator embeds RENAME commands before the actual LOOKUP JOIN to align
// left-side key columns with lookup index key names. Process these renames so that
// fields renamed from non-index-mapped sources correctly inherit indexMapped=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.elasticsearch.xpack.esql.generator.command.pipe.GrokGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.InlineStatsGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.KeepGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.LimitByGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.LimitGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.LookupJoinGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.MvExpandGenerator;
Expand Down Expand Up @@ -114,6 +115,7 @@ public class EsqlQueryGenerator {
GrokGenerator.INSTANCE,
KeepGenerator.INSTANCE,
InlineStatsGenerator.INSTANCE,
LimitByGenerator.INSTANCE,
LimitGenerator.INSTANCE,
LookupJoinGenerator.INSTANCE,
MvExpandGenerator.INSTANCE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import java.util.List;

public class InlineStatsGenerator extends StatsGenerator {
public static final String INLINE_STATS = "inline stats";
public static final String INLINE_STATS = "inline_stats";
public static final CommandGenerator INSTANCE = new InlineStatsGenerator();

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* 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.generator.command.pipe;

import org.elasticsearch.xpack.esql.generator.Column;
import org.elasticsearch.xpack.esql.generator.EsqlQueryGenerator;
import org.elasticsearch.xpack.esql.generator.QueryExecutor;
import org.elasticsearch.xpack.esql.generator.command.CommandGenerator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.elasticsearch.test.ESTestCase.randomIntBetween;

public class LimitByGenerator implements CommandGenerator {
public static final CommandGenerator INSTANCE = new LimitByGenerator();
public static final String LIMIT_BY = "limit_by";

private static final String LIMIT_CONTEXT = "limit";
private static final String GROUPINGS_CONTEXT = "groupings";

@Override
public CommandDescription generate(
List<CommandDescription> previousCommands,
List<Column> previousOutput,
QuerySchema schema,
QueryExecutor executor
) {
List<Column> groupable = previousOutput.stream()
.filter(EsqlQueryGenerator::groupable)
.filter(EsqlQueryGenerator::fieldCanBeUsed)
.toList();
if (groupable.isEmpty()) {
return EMPTY_DESCRIPTION;
}

int limit = randomIntBetween(0, 100);
int groupingCount = randomIntBetween(1, Math.min(3, groupable.size()));
Set<String> groupings = new LinkedHashSet<>();
for (int i = 0; i < groupingCount; i++) {
String col = EsqlQueryGenerator.randomGroupableName(groupable);
if (col != null) {
groupings.add(col);
}
}
if (groupings.isEmpty()) {
return EMPTY_DESCRIPTION;
}

String cmd = " | LIMIT " + limit + " BY " + String.join(", ", groupings);
return new CommandDescription(LIMIT_BY, this, cmd, Map.of(LIMIT_CONTEXT, limit, GROUPINGS_CONTEXT, List.copyOf(groupings)));
}

@Override
public ValidationResult validateOutput(
List<CommandDescription> previousCommands,
CommandDescription commandDescription,
List<Column> previousColumns,
List<List<Object>> previousOutput,
List<Column> columns,
List<List<Object>> output
) {
int limit = (int) commandDescription.context().get(LIMIT_CONTEXT);

if (limit == 0 && output.isEmpty() == false) {
return new ValidationResult(false, "LIMIT 0 BY should return no rows, got [" + output.size() + "]");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not checking expectSameColumns in this case. Should we?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any of them fail, we'll report a failure. We can check both things and make a composed message, but I think it's not worth it here (?)

}

ValidationResult columnsResult = CommandGenerator.expectSameColumns(previousCommands, previousColumns, columns);
if (columnsResult.success() == false) {
return columnsResult;
}

return validatePerGroupRowCounts(commandDescription, columns, output, limit);
}

@SuppressWarnings("unchecked")
private static ValidationResult validatePerGroupRowCounts(
CommandDescription commandDescription,
List<Column> columns,
List<List<Object>> output,
int limit
) {
List<String> groupings = (List<String>) commandDescription.context().get(GROUPINGS_CONTEXT);

List<Integer> groupingIndices = new ArrayList<>(groupings.size());
for (String grouping : groupings) {
String rawName = EsqlQueryGenerator.unquote(grouping);
int idx = -1;
for (int i = 0; i < columns.size(); i++) {
if (columns.get(i).name().equals(rawName)) {
idx = i;
break;
}
}
if (idx == -1) {
return new ValidationResult(
false,
"LIMIT "
+ limit
+ " BY: grouping column ["
+ rawName
+ "] was not found in the output schema. Available columns: "
+ columns.stream().map(Column::name).toList()
);
}
groupingIndices.add(idx);
}

Map<List<Object>, Integer> groupCounts = new HashMap<>();
for (List<Object> row : output) {
List<Object> key = new ArrayList<>(groupingIndices.size());
for (int idx : groupingIndices) {
Object value = row.get(idx);
key.add(value);
}
groupCounts.merge(key, 1, Integer::sum);
}

for (var entry : groupCounts.entrySet()) {
if (entry.getValue() > limit) {
return new ValidationResult(
false,
"LIMIT "
+ limit
+ " BY: group "
+ entry.getKey()
+ " has ["
+ entry.getValue()
+ "] rows, expected at most ["
+ limit
+ "]"
);
}
}

return VALIDATION_OK;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public CommandDescription generate(
return EMPTY_DESCRIPTION;
}
StringBuilder cmd = new StringBuilder(" | ");
cmd.append(commandName());
cmd.append(commandName().replace("_", " "));
cmd.append(" ");
int nStats = randomIntBetween(1, 5);
for (int i = 0; i < nStats; i++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@

import org.elasticsearch.xpack.esql.generator.Column;
import org.elasticsearch.xpack.esql.generator.command.CommandGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.ChangePointGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.InlineStatsGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.LimitByGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.LimitGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.MvExpandGenerator;
import org.elasticsearch.xpack.esql.generator.command.pipe.StatsGenerator;

import java.util.HashSet;
import java.util.List;
Expand All @@ -29,6 +35,18 @@ private FullTextFunctionGenerator() {}

private static final Set<String> QSTR_KQL_SAFE_COMMANDS = Set.of("from", "where", "sort");

/**
* Commands after which full-text expressions (match, qstr, kql, etc.) are not allowed.
*/
private static final Set<String> FULL_TEXT_FORBIDDEN_AFTER_COMMANDS = Set.of(
LimitGenerator.LIMIT,
LimitByGenerator.LIMIT_BY,
StatsGenerator.STATS,
InlineStatsGenerator.INLINE_STATS,
ChangePointGenerator.CHANGE_POINT,
MvExpandGenerator.MV_EXPAND
);

private static boolean isFullTextAllowed(List<CommandGenerator.CommandDescription> previousCommands) {
if (previousCommands == null || previousCommands.isEmpty()) {
return false;
Expand All @@ -37,11 +55,7 @@ private static boolean isFullTextAllowed(List<CommandGenerator.CommandDescriptio
return false;
}
for (CommandGenerator.CommandDescription cmd : previousCommands) {
if ("limit".equals(cmd.commandName())
|| "stats".equals(cmd.commandName())
|| "inline stats".equals(cmd.commandName())
|| "change_point".equals(cmd.commandName())
|| "mv_expand".equals(cmd.commandName())) {
if (FULL_TEXT_FORBIDDEN_AFTER_COMMANDS.contains(cmd.commandName())) {
return false;
}
}
Expand Down
Loading