diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/RemoteIndexResolutionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/RemoteIndexResolutionIT.java index d37f68494ebba..085f58d6c09b2 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/RemoteIndexResolutionIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/plugin/RemoteIndexResolutionIT.java @@ -7,15 +7,18 @@ package org.elasticsearch.xpack.esql.plugin; -import org.elasticsearch.ElasticsearchTimeoutException; +import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.index.query.MatchNoneQueryBuilder; import org.elasticsearch.transport.NoSuchRemoteClusterException; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.action.AbstractCrossClusterTestCase; +import org.elasticsearch.xpack.esql.action.EsqlExecutionInfo.Cluster.Status; import org.elasticsearch.xpack.esql.action.EsqlQueryAction; import org.elasticsearch.xpack.esql.action.EsqlQueryRequest; import org.elasticsearch.xpack.esql.action.EsqlQueryResponse; import org.elasticsearch.xpack.esql.core.expression.MetadataAttribute; +import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -29,9 +32,14 @@ public class RemoteIndexResolutionIT extends AbstractCrossClusterTestCase { public void testResolvesRemoteIndex() { indexRandom(REMOTE_CLUSTER_1, true, "index-1", 1); - try (var response = run(syncEsqlQueryRequest().query("FROM " + REMOTE_CLUSTER_1 + ":index-1 METADATA _index"))) { + try ( + var response = run( + syncEsqlQueryRequest().query("FROM " + REMOTE_CLUSTER_1 + ":index-1 METADATA _index").includeCCSMetadata(true) + ) + ) { assertOk(response); assertResultConcreteIndices(response, REMOTE_CLUSTER_1 + ":index-1"); + assertExecutionInfo(response, new EsqlResponseExecutionInfo(REMOTE_CLUSTER_1, "index-1", Status.SUCCESSFUL)); } } @@ -40,6 +48,17 @@ public void testResolveRemoteUnknownIndex() { // This index is mixed into the resultset to test error handling of the missing concrete remote, not the empty result. indexRandom(LOCAL_CLUSTER, true, "data", 1); + expectThrows( + VerificationException.class, + containsString("Unknown index [" + REMOTE_CLUSTER_1 + ":fake]"), + () -> run(syncEsqlQueryRequest().query("FROM data," + REMOTE_CLUSTER_1 + ":fake")) + ); + expectThrows( + VerificationException.class, + containsString("Unknown index [" + REMOTE_CLUSTER_1 + ":fake]"), + () -> run(syncEsqlQueryRequest().query("FROM data," + REMOTE_CLUSTER_1 + ":fake").allowPartialResults(true)) + ); + setSkipUnavailable(REMOTE_CLUSTER_1, false); expectThrows( VerificationException.class, @@ -48,15 +67,33 @@ public void testResolveRemoteUnknownIndex() { ); setSkipUnavailable(REMOTE_CLUSTER_1, true); - try (var response = run(syncEsqlQueryRequest().query("FROM data," + REMOTE_CLUSTER_1 + ":fake METADATA _index"))) { + try ( + var response = run( + syncEsqlQueryRequest().query("FROM data," + REMOTE_CLUSTER_1 + ":fake METADATA _index").includeCCSMetadata(true) + ) + ) { assertPartial(response); assertResultConcreteIndices(response, "data"); + assertExecutionInfo( + response, + new EsqlResponseExecutionInfo(LOCAL_CLUSTER, "data", Status.SUCCESSFUL), + new EsqlResponseExecutionInfo(REMOTE_CLUSTER_1, "fake", Status.SKIPPED) + ); } setSkipUnavailable(REMOTE_CLUSTER_1, null); - try (var response = run(syncEsqlQueryRequest().query("FROM data," + REMOTE_CLUSTER_1 + ":fake METADATA _index"))) { + try ( + var response = run( + syncEsqlQueryRequest().query("FROM data," + REMOTE_CLUSTER_1 + ":fake METADATA _index").includeCCSMetadata(true) + ) + ) { assertPartial(response); assertResultConcreteIndices(response, "data"); + assertExecutionInfo( + response, + new EsqlResponseExecutionInfo(LOCAL_CLUSTER, "data", Status.SUCCESSFUL), + new EsqlResponseExecutionInfo(REMOTE_CLUSTER_1, "fake", Status.SKIPPED) + ); } } @@ -64,9 +101,18 @@ public void testResolvesLocalAndRemoteIndex() { indexRandom(LOCAL_CLUSTER, true, "index-1", 1); indexRandom(REMOTE_CLUSTER_1, true, "index-1", 1); - try (var response = run(syncEsqlQueryRequest().query("FROM index-1," + REMOTE_CLUSTER_1 + ":index-1 METADATA _index"))) { + try ( + var response = run( + syncEsqlQueryRequest().query("FROM index-1," + REMOTE_CLUSTER_1 + ":index-1 METADATA _index").includeCCSMetadata(true) + ) + ) { assertOk(response); assertResultConcreteIndices(response, "index-1", REMOTE_CLUSTER_1 + ":index-1"); + assertExecutionInfo( + response, + new EsqlResponseExecutionInfo(LOCAL_CLUSTER, "index-1", Status.SUCCESSFUL), + new EsqlResponseExecutionInfo(REMOTE_CLUSTER_1, "index-1", Status.SUCCESSFUL) + ); } } @@ -75,13 +121,19 @@ public void testResolvesRemotesWithPattern() { indexRandom(REMOTE_CLUSTER_1, true, "index-1", 1); indexRandom(REMOTE_CLUSTER_2, true, "index-1", 1); - try (var response = run(syncEsqlQueryRequest().query("FROM *:index-1 METADATA _index"))) { + try (var response = run(syncEsqlQueryRequest().query("FROM *:index-1 METADATA _index").includeCCSMetadata(true))) { assertOk(response); assertResultConcreteIndices(response, REMOTE_CLUSTER_1 + ":index-1", REMOTE_CLUSTER_2 + ":index-1"); // local is not included + assertExecutionInfo( + response, + new EsqlResponseExecutionInfo(REMOTE_CLUSTER_1, "index-1", Status.SUCCESSFUL), + new EsqlResponseExecutionInfo(REMOTE_CLUSTER_2, "index-1", Status.SUCCESSFUL) + ); } - try (var response = run(syncEsqlQueryRequest().query("FROM fake*:index-1 METADATA _index"))) { + try (var response = run(syncEsqlQueryRequest().query("FROM fake*:index-1 METADATA _index").includeCCSMetadata(true))) { assertOk(response); assertResultConcreteIndices(response); // empty + assertExecutionInfo(response); // empty } } @@ -93,14 +145,33 @@ public void testDoesNotResolvesUnknownRemote() { ); } - private EsqlQueryResponse run(EsqlQueryRequest request) { - try { - return client(LOCAL_CLUSTER).execute(EsqlQueryAction.INSTANCE, request).actionGet(30, TimeUnit.SECONDS); - } catch (ElasticsearchTimeoutException e) { - throw new AssertionError("timeout", e); + public void testResolutionWithFilter() { + indexRandom(REMOTE_CLUSTER_1, true, "index-1", 1); + + try ( + var response = run( + syncEsqlQueryRequest().query("FROM " + REMOTE_CLUSTER_1 + ":index-1 METADATA _index").filter(new MatchAllQueryBuilder()) + ) + ) { + assertOk(response); + assertResultConcreteIndices(response, REMOTE_CLUSTER_1 + ":index-1"); + assertExecutionInfo(response, new EsqlResponseExecutionInfo(REMOTE_CLUSTER_1, "index-1", Status.SUCCESSFUL)); + } + try ( + var response = run( + syncEsqlQueryRequest().query("FROM " + REMOTE_CLUSTER_1 + ":index-1 METADATA _index").filter(new MatchNoneQueryBuilder()) + ) + ) { + assertOk(response); + assertResultConcreteIndices(response); + assertExecutionInfo(response, new EsqlResponseExecutionInfo(REMOTE_CLUSTER_1, "index-1", Status.SUCCESSFUL)); } } + private EsqlQueryResponse run(EsqlQueryRequest request) { + return client(LOCAL_CLUSTER).execute(EsqlQueryAction.INSTANCE, request).actionGet(30, TimeUnit.SECONDS); + } + private void indexRandom(String clusterAlias, boolean forceRefresh, String index, int numDocs) { assert numDocs == 1; var client = client(clusterAlias); @@ -123,6 +194,10 @@ private static void assertResultConcreteIndices(EsqlQueryResponse response, Obje assertThat(() -> response.column(indexColumn), containsInAnyOrder(indices)); } + private static void assertExecutionInfo(EsqlQueryResponse response, EsqlResponseExecutionInfo... infos) { + assertThat(executionInfo(response), containsInAnyOrder(infos)); + } + private static int findIndexColumn(EsqlQueryResponse response) { for (int c = 0; c < response.columns().size(); c++) { if (Objects.equals(response.columns().get(c).name(), MetadataAttribute.INDEX)) { @@ -131,4 +206,15 @@ private static int findIndexColumn(EsqlQueryResponse response) { } throw new AssertionError("no _index column found"); } + + private static List executionInfo(EsqlQueryResponse response) { + return response.getExecutionInfo() + .getClusters() + .values() + .stream() + .map(cluster -> new EsqlResponseExecutionInfo(cluster.getClusterAlias(), cluster.getIndexExpression(), cluster.getStatus())) + .toList(); + } + + private record EsqlResponseExecutionInfo(String alias, String index, Status status) {} } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryRequest.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryRequest.java index b106a9a2d34b7..8b71536bcf7f9 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryRequest.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlQueryRequest.java @@ -143,16 +143,18 @@ public void profile(boolean profile) { this.profile = profile; } - public void includeCCSMetadata(Boolean include) { + public EsqlQueryRequest includeCCSMetadata(Boolean include) { this.includeCCSMetadata = include; + return this; } public Boolean includeCCSMetadata() { return includeCCSMetadata; } - public void includeExecutionMetadata(Boolean include) { + public EsqlQueryRequest includeExecutionMetadata(Boolean include) { this.includeExecutionMetadata = include; + return this; } public Boolean includeExecutionMetadata() {