diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/session/ResolvedIndexExpressionIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/session/ResolvedIndexExpressionIT.java new file mode 100644 index 0000000000000..35efdf1b66639 --- /dev/null +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/session/ResolvedIndexExpressionIT.java @@ -0,0 +1,89 @@ +/* + * 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.session; + +import org.elasticsearch.common.Strings; +import org.elasticsearch.xpack.esql.action.AbstractCrossClusterTestCase; + +import java.util.Map; +import java.util.Set; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasEntry; + +public class ResolvedIndexExpressionIT extends AbstractCrossClusterTestCase { + + public void testLocalIndices() { + createIndex(LOCAL_CLUSTER, "index-1"); + + assertThat(resolveIndices("index-1"), hasEntry(LOCAL_CLUSTER, resolvedIndexExpression("index-1", "index-1"))); + } + + public void testLocalAlias() { + createIndex(LOCAL_CLUSTER, "index-1"); + createAlias(LOCAL_CLUSTER, "alias-1", "index-1"); + + assertThat(resolveIndices("alias-1"), hasEntry(LOCAL_CLUSTER, resolvedIndexExpression("alias-1", "index-1"))); + } + + public void testLocalPattern() { + createIndex(LOCAL_CLUSTER, "index-1"); + createIndex(LOCAL_CLUSTER, "index-2"); + + assertThat(resolveIndices("index-*"), hasEntry(LOCAL_CLUSTER, resolvedIndexExpression("index-*", "index-1,index-2"))); + } + + public void testLocalMultiple() { + createIndex(LOCAL_CLUSTER, "index-1"); + createIndex(LOCAL_CLUSTER, "index-2"); + + assertThat( + resolveIndices("index-1,index-2"), + hasEntry(LOCAL_CLUSTER, resolvedIndexExpression("index-1,index-2", "index-1,index-2")) + ); + } + + public void testLocalAndRemote() { + createIndex(LOCAL_CLUSTER, "index-1"); + createIndex(REMOTE_CLUSTER_1, "index-2"); + createIndex(REMOTE_CLUSTER_2, "index-3"); + + assertThat( + resolveIndices("index-*,*:index-*"), + allOf( + hasEntry(LOCAL_CLUSTER, resolvedIndexExpression("index-*", "index-1")), + hasEntry(REMOTE_CLUSTER_1, resolvedIndexExpression("index-*", "index-2")), + hasEntry(REMOTE_CLUSTER_2, resolvedIndexExpression("index-*", "index-3")) + ) + ); + } + + private Map resolveIndices(String indices) { + return ResolvedIndexExpression.from( + client(LOCAL_CLUSTER).prepareFieldCaps(Strings.splitStringByCommaToArray(indices)) + .setFields("*") + .setIncludeResolvedTo(true) + .get() + ); + } + + private void createIndex(String clusterAlias, String index) { + client(clusterAlias).admin().indices().prepareCreate(index).get(); + } + + private void createAlias(String clusterAlias, String alias, String index) { + client(clusterAlias).admin().indices().prepareAliases(TEST_REQUEST_TIMEOUT, TEST_REQUEST_TIMEOUT).addAlias(index, alias).get(); + } + + private static ResolvedIndexExpression resolvedIndexExpression(String expression, String resolved) { + return new ResolvedIndexExpression( + Set.of(Strings.splitStringByCommaToArray(expression)), + Set.of(Strings.splitStringByCommaToArray(resolved)) + ); + } +} diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/ResolvedIndexExpression.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/ResolvedIndexExpression.java new file mode 100644 index 0000000000000..71bed2aeab39e --- /dev/null +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/ResolvedIndexExpression.java @@ -0,0 +1,46 @@ +/* + * 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.session; + +import org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponse; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.transport.RemoteClusterAware; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import static java.util.stream.Collectors.toMap; + +public record ResolvedIndexExpression(Set expression, Set resolved) { + + private static final ResolvedIndexExpression EMPTY = new ResolvedIndexExpression(Set.of(), Set.of()); + + public static Map from(FieldCapabilitiesResponse response) { + return Stream.concat( + Stream.of(Map.entry(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY, response.getResolvedLocally())), + response.getResolvedRemotely().entrySet().stream() + ) + .map( + entry -> Map.entry( + entry.getKey(), + entry.getValue() + .expressions() + .stream() + .filter(e -> e.localExpressions().indices().isEmpty() == false) + .map(e -> new ResolvedIndexExpression(Set.of(e.original()), e.localExpressions().indices())) + .reduce(EMPTY, ResolvedIndexExpression::merge) + ) + ) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static ResolvedIndexExpression merge(ResolvedIndexExpression a, ResolvedIndexExpression b) { + return new ResolvedIndexExpression(Sets.union(a.expression(), b.expression()), Sets.union(a.resolved(), b.resolved())); + } +}