From e0f5989bc7e32af2a875b2b4e6762ead0cfb8de6 Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Fri, 5 Sep 2025 13:25:37 +0200 Subject: [PATCH 01/10] resolve original index pattern --- ...ossClusterQueriesWithInvalidLicenseIT.java | 1 + .../esql/action/CrossClusterQueryIT.java | 8 ++--- .../xpack/esql/session/EsqlSession.java | 32 ++++--------------- 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java index be19180e1b4ad..84b9d853e4e38 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java @@ -58,6 +58,7 @@ public void testMetadataCrossClusterQuery() { assertThat(e.getMessage(), containsString(LICENSE_ERROR_MESSAGE)); } + @AwaitsFix(bugUrl = "es-12487") public void testQueryAgainstNonMatchingClusterWildcardPattern() { Tuple includeCCSMetadata = randomIncludeCCSMetadata(); Boolean requestIncludeMeta = includeCCSMetadata.v1(); diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueryIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueryIT.java index b4a5c82f74b93..6d3fd72a71cfe 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueryIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueryIT.java @@ -398,24 +398,24 @@ public void testSearchesAgainstNonMatchingIndices() throws Exception { // an error is thrown if there is a concrete index that does not match { String q = "FROM nomatch*,cluster-a:nomatch"; - String expectedError = "Unknown index [cluster-a:nomatch,nomatch*]"; + String expectedError = "Unknown index [nomatch*,cluster-a:nomatch]"; expectVerificationExceptionForQuery(q, expectedError, requestIncludeMeta); } // an error is thrown if there are no matching indices at all - local with wildcard, remote with wildcard { String q = "FROM nomatch*,cluster-a:nomatch*"; - String expectedError = "Unknown index [cluster-a:nomatch*,nomatch*]"; + String expectedError = "Unknown index [nomatch*,cluster-a:nomatch*]"; expectVerificationExceptionForQuery(q, expectedError, requestIncludeMeta); } { String q = "FROM nomatch,cluster-a:nomatch"; - String expectedError = "Unknown index [cluster-a:nomatch,nomatch]"; + String expectedError = "Unknown index [nomatch,cluster-a:nomatch]"; expectVerificationExceptionForQuery(q, expectedError, requestIncludeMeta); } { String q = "FROM nomatch,cluster-a:nomatch*"; - String expectedError = "Unknown index [cluster-a:nomatch*,nomatch]"; + String expectedError = "Unknown index [nomatch,cluster-a:nomatch*]"; expectVerificationExceptionForQuery(q, expectedError, requestIncludeMeta); } diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java index 3d0a4ab26b8d6..e3a5edbef39ce 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java @@ -633,23 +633,10 @@ private void preAnalyzeMainIndices( ThreadPool.Names.SEARCH_COORDINATION, ThreadPool.Names.SYSTEM_READ ); - // TODO we plan to support joins in the future when possible, but for now we'll just fail early if we see one - List indices = preAnalysis.indices; - if (indices.size() > 1) { - // Note: JOINs are not supported but we detect them when - listener.onFailure(new MappingException("Queries with multiple indices are not supported")); - } else if (indices.size() == 1) { - IndexPattern table = indices.getFirst(); - - // if the preceding call to the enrich policy API found unavailable clusters, recreate the index expression to search - // based only on available clusters (which could now be an empty list) - String indexExpressionToResolve = EsqlCCSUtils.createIndexExpressionFromAvailableClusters(executionInfo); - if (indexExpressionToResolve.isEmpty()) { - // if this was a pure remote CCS request (no local indices) and all remotes are offline, return an empty IndexResolution - listener.onResponse( - result.withIndexResolution(IndexResolution.valid(new EsIndex(table.indexPattern(), Map.of(), Map.of()))) - ); - } else { + switch (preAnalysis.indices.size()) { + // occurs when dealing with local relations (row a = 1) + case 0 -> listener.onResponse(result.withIndexResolution(IndexResolution.invalid("[none specified]"))); + case 1 -> { boolean includeAllDimensions = false; // call the EsqlResolveFieldsAction (field-caps) to resolve indices and get field types if (preAnalysis.indexMode == IndexMode.TIME_SERIES) { @@ -663,7 +650,7 @@ private void preAnalyzeMainIndices( } } indexResolver.resolveAsMergedMapping( - indexExpressionToResolve, + preAnalysis.indices.getFirst().indexPattern(), result.fieldNames, requestFilter, includeAllDimensions, @@ -672,13 +659,8 @@ private void preAnalyzeMainIndices( }) ); } - } else { - try { - // occurs when dealing with local relations (row a = 1) - listener.onResponse(result.withIndexResolution(IndexResolution.invalid("[none specified]"))); - } catch (Exception ex) { - listener.onFailure(ex); - } + // Note: JOINs are not supported but we detect them when + default -> listener.onFailure(new MappingException("Queries with multiple indices are not supported")); } } From e172c1320980a9a90dcb74ff0118eb988595d081 Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Mon, 8 Sep 2025 11:28:44 +0200 Subject: [PATCH 02/10] fix csv tests --- .../src/test/java/org/elasticsearch/xpack/esql/CsvTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java index 2ff6ce71be516..a993b3a5f4420 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/CsvTests.java @@ -530,12 +530,12 @@ private LogicalPlan analyzedPlan(LogicalPlan parsed, CsvTestsDataLoader.MultiInd private static CsvTestsDataLoader.MultiIndexTestDataset testDatasets(LogicalPlan parsed) { var preAnalysis = new PreAnalyzer().preAnalyze(parsed); - if (preAnalysis.index() == null) { + if (preAnalysis.indexPattern() == null) { // If the data set doesn't matter we'll just grab one we know works. Employees is fine. return CsvTestsDataLoader.MultiIndexTestDataset.of(CSV_DATASET_MAP.get("employees")); } - String indexName = preAnalysis.index().indexPattern(); + String indexName = preAnalysis.indexPattern().indexPattern(); List datasets = new ArrayList<>(); if (indexName.endsWith("*")) { String indexPrefix = indexName.substring(0, indexName.length() - 1); From 21ccd63a25ba3c4e20beb9e5ca3dd45c20cb84aa Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Wed, 17 Sep 2025 15:29:26 +0200 Subject: [PATCH 03/10] returnLocalAll=false --- ...ossClusterQueriesWithInvalidLicenseIT.java | 45 ++++++++++--------- .../xpack/esql/session/IndexResolver.java | 1 + 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java index 84b9d853e4e38..517f3e1fd5860 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java @@ -10,6 +10,7 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.core.Tuple; import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.plan.logical.Enrich; import java.util.ArrayList; @@ -58,7 +59,6 @@ public void testMetadataCrossClusterQuery() { assertThat(e.getMessage(), containsString(LICENSE_ERROR_MESSAGE)); } - @AwaitsFix(bugUrl = "es-12487") public void testQueryAgainstNonMatchingClusterWildcardPattern() { Tuple includeCCSMetadata = randomIncludeCCSMetadata(); Boolean requestIncludeMeta = includeCCSMetadata.v1(); @@ -66,25 +66,30 @@ public void testQueryAgainstNonMatchingClusterWildcardPattern() { // since this wildcarded expression does not resolve to a valid remote cluster, it is not considered // a cross-cluster search and thus should not throw a license error - String q = "FROM xremote*:events"; - { - String limit1 = q + " | STATS count(*)"; - try (EsqlQueryResponse resp = runQuery(limit1, requestIncludeMeta)) { - assertThat(resp.columns().size(), equalTo(1)); - EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); - assertThat(executionInfo.isCrossClusterSearch(), is(false)); - assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); - } - - String limit0 = q + " | LIMIT 0"; - try (EsqlQueryResponse resp = runQuery(limit0, requestIncludeMeta)) { - assertThat(resp.columns().size(), equalTo(1)); - assertThat(getValuesList(resp).size(), equalTo(0)); - EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); - assertThat(executionInfo.isCrossClusterSearch(), is(false)); - assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); - } - } + expectThrows( + VerificationException.class, + containsString("Unknown index [xremote*:events]"), + () -> runQuery("FROM xremote*:events | STATS count(*)", requestIncludeMeta).close() + ); + // try (EsqlQueryResponse resp = runQuery("FROM xremote*:events | STATS count(*)", requestIncludeMeta)) { + // assertThat(resp.columns().size(), equalTo(1)); + // EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); + // assertThat(executionInfo.isCrossClusterSearch(), is(false)); + // assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); + // } + + expectThrows( + VerificationException.class, + containsString("Unknown index [xremote*:events]"), + () -> runQuery("FROM xremote*:events | STATS count(*)", requestIncludeMeta).close() + ); + // try (EsqlQueryResponse resp = runQuery("FROM xremote*:events | LIMIT 0", requestIncludeMeta)) { + // assertThat(resp.columns().size(), equalTo(1)); + // assertThat(getValuesList(resp).size(), equalTo(0)); + // EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); + // assertThat(executionInfo.isCrossClusterSearch(), is(false)); + // assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); + // } } public void testCCSWithLimit0() { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java index 2771afb6ef3ca..909fb3d71ecc1 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/IndexResolver.java @@ -309,6 +309,7 @@ private static FieldCapabilitiesRequest createFieldCapsRequest( req.fields(fieldNames.toArray(String[]::new)); req.includeUnmapped(true); req.indexFilter(requestFilter); + req.returnLocalAll(false); // lenient because we throw our own errors looking at the response e.g. if something was not resolved // also because this way security doesn't throw authorization exceptions but rather honors ignore_unavailable req.indicesOptions(FIELD_CAPS_INDICES_OPTIONS); From 24b7a4c15e49b55bddad24ec8cdd857c59165359 Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Wed, 17 Sep 2025 16:13:11 +0200 Subject: [PATCH 04/10] move execution info updates --- .../xpack/esql/session/EsqlSession.java | 75 +++++++++---------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java index 9b8142cc458f5..e73c1aee50a13 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java @@ -654,25 +654,30 @@ private void preAnalyzeMainIndices( ThreadPool.Names.SYSTEM_READ ); if (preAnalysis.indexPattern() != null) { - boolean includeAllDimensions = false; - // call the EsqlResolveFieldsAction (field-caps) to resolve indices and get field types - if (preAnalysis.indexMode() == IndexMode.TIME_SERIES) { - includeAllDimensions = true; - // TODO: Maybe if no indices are returned, retry without index mode and provide a clearer error message. - var indexModeFilter = new TermQueryBuilder(IndexModeFieldMapper.NAME, IndexMode.TIME_SERIES.getName()); - if (requestFilter != null) { - requestFilter = new BoolQueryBuilder().filter(requestFilter).filter(indexModeFilter); - } else { - requestFilter = indexModeFilter; - } - } indexResolver.resolveAsMergedMapping( preAnalysis.indexPattern().indexPattern(), result.fieldNames, - requestFilter, - includeAllDimensions, - listener.delegateFailure((l, indexResolution) -> { - l.onResponse(result.withIndexResolution(indexResolution)); + switch (preAnalysis.indexMode()) { + case TIME_SERIES -> { + var indexModeFilter = new TermQueryBuilder(IndexModeFieldMapper.NAME, IndexMode.TIME_SERIES.getName()); + yield requestFilter != null + ? new BoolQueryBuilder().filter(requestFilter).filter(indexModeFilter) + : indexModeFilter; + } + default -> requestFilter; + }, + preAnalysis.indexMode() == IndexMode.TIME_SERIES, + listener.delegateFailure((l, mainIndexResolution) -> { + // the order here is tricky - if the cluster has been filtered and later became unavailable, + // do we want to declare it successful or skipped? For now, unavailability takes precedence. + EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, mainIndexResolution.failures()); + EsqlCCSUtils.updateExecutionInfoWithClustersWithNoMatchingIndices( + executionInfo, + mainIndexResolution, + requestFilter != null + ); + + l.onResponse(result.withIndexResolution(mainIndexResolution)); }) ); } else { @@ -689,29 +694,21 @@ private void analyzeWithRetry( PreAnalysisResult result, ActionListener listener ) { - if (result.indices.isValid()) { - EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, result.indices.failures()); - if (executionInfo.isCrossClusterSearch() - && executionInfo.getClusterStates(EsqlExecutionInfo.Cluster.Status.RUNNING).findAny().isEmpty()) { - // for a CCS, if all clusters have been marked as SKIPPED, nothing to search so send a sentinel Exception - // to let the LogicalPlanActionListener decide how to proceed - LOGGER.debug("No more clusters to search, ending analysis stage"); - listener.onFailure(new NoClustersToSearchException()); - return; - } - } + // if (result.indices.isValid()) { + // EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, result.indices.failures()); + // if (executionInfo.isCrossClusterSearch() + // && executionInfo.getClusterStates(EsqlExecutionInfo.Cluster.Status.RUNNING).findAny().isEmpty()) { + // // for a CCS, if all clusters have been marked as SKIPPED, nothing to search so send a sentinel Exception + // // to let the LogicalPlanActionListener decide how to proceed + // LOGGER.debug("No more clusters to search, ending analysis stage"); + // listener.onFailure(new NoClustersToSearchException()); + // return; + // } + // } var description = requestFilter == null ? "the only attempt without filter" : "first attempt with filter"; - LOGGER.debug("Analyzing the plan ({})", description); - try { - if (result.indices.isValid() || requestFilter != null) { - // We won't run this check with no filter and no valid indices since this may lead to false positive - missing index report - // when the resolution result is not valid for a different reason. - if (executionInfo.clusterInfo.isEmpty() == false) { - EsqlCCSUtils.updateExecutionInfoWithClustersWithNoMatchingIndices(executionInfo, result.indices, requestFilter != null); - } - } + LOGGER.debug("Analyzing the plan ({})", description); LogicalPlan plan = analyzedPlan(parsed, result, executionInfo); LOGGER.debug("Analyzed plan ({}):\n{}", description, plan); // the analysis succeeded from the first attempt, irrespective if it had a filter or not, just continue with the planning @@ -724,12 +721,8 @@ private void analyzeWithRetry( } else { // retrying and make the index resolution work without any index filtering. preAnalyzeMainIndices(preAnalysis, executionInfo, result, null, listener.delegateFailure((l, r) -> { - LOGGER.debug("Analyzing the plan (second attempt, without filter)"); try { - // the order here is tricky - if the cluster has been filtered and later became unavailable, - // do we want to declare it successful or skipped? For now, unavailability takes precedence. - EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, r.indices.failures()); - EsqlCCSUtils.updateExecutionInfoWithClustersWithNoMatchingIndices(executionInfo, r.indices, false); + LOGGER.debug("Analyzing the plan (second attempt, without filter)"); LogicalPlan plan = analyzedPlan(parsed, r, executionInfo); LOGGER.debug("Analyzed plan (second attempt without filter):\n{}", plan); l.onResponse(plan); From 3029bfb518a72060e1c7d1cedd339cfe8771f779 Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Tue, 23 Sep 2025 13:34:53 +0200 Subject: [PATCH 05/10] resolve using original index pattern --- .../xpack/esql/session/EsqlCCSUtils.java | 24 -------- .../xpack/esql/session/EsqlSession.java | 46 +++++++-------- .../xpack/esql/session/EsqlCCSUtilsTests.java | 56 ------------------- 3 files changed, 19 insertions(+), 107 deletions(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtils.java index a1cba3bff0a4e..d0aaac39e8cfc 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtils.java @@ -156,30 +156,6 @@ static void updateExecutionInfoToReturnEmptyResult(EsqlExecutionInfo executionIn } } - static String createIndexExpressionFromAvailableClusters(EsqlExecutionInfo executionInfo) { - StringBuilder sb = new StringBuilder(); - for (String clusterAlias : executionInfo.clusterAliases()) { - EsqlExecutionInfo.Cluster cluster = executionInfo.getCluster(clusterAlias); - // Exclude clusters which are either skipped or have no indices matching wildcard, or filtered out. - if (cluster.getStatus() != Cluster.Status.SKIPPED && cluster.getStatus() != Cluster.Status.SUCCESSFUL) { - if (cluster.getClusterAlias().equals(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) { - sb.append(executionInfo.getCluster(clusterAlias).getIndexExpression()).append(','); - } else { - String indexExpression = executionInfo.getCluster(clusterAlias).getIndexExpression(); - for (String index : indexExpression.split(",")) { - sb.append(clusterAlias).append(':').append(index).append(','); - } - } - } - } - - if (sb.length() > 0) { - return sb.substring(0, sb.length() - 1); - } else { - return ""; - } - } - static String createQualifiedLookupIndexExpressionFromAvailableClusters(EsqlExecutionInfo executionInfo, String localPattern) { if (executionInfo.getClusters().isEmpty()) { return localPattern; diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java index fa14952bf1b50..0990199adf6f2 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java @@ -647,33 +647,25 @@ private void preAnalyzeMainIndices( ThreadPool.Names.SYSTEM_READ ); if (preAnalysis.indexPattern() != null) { - String indexExpressionToResolve = EsqlCCSUtils.createIndexExpressionFromAvailableClusters(executionInfo); - if (indexExpressionToResolve.isEmpty()) { - // if this was a pure remote CCS request (no local indices) and all remotes are offline, return an empty IndexResolution - listener.onResponse( - result.withIndices(IndexResolution.valid(new EsIndex(preAnalysis.indexPattern().indexPattern(), Map.of(), Map.of()))) - ); - } else { - indexResolver.resolveAsMergedMapping( - indexExpressionToResolve, - result.fieldNames, - // Maybe if no indices are returned, retry without index mode and provide a clearer error message. - switch (preAnalysis.indexMode()) { - case IndexMode.TIME_SERIES -> { - var indexModeFilter = new TermQueryBuilder(IndexModeFieldMapper.NAME, IndexMode.TIME_SERIES.getName()); - yield requestFilter != null - ? new BoolQueryBuilder().filter(requestFilter).filter(indexModeFilter) - : indexModeFilter; - } - default -> requestFilter; - }, - preAnalysis.indexMode() == IndexMode.TIME_SERIES, - listener.delegateFailureAndWrap((l, indexResolution) -> { - EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, indexResolution.failures()); - l.onResponse(result.withIndices(indexResolution)); - }) - ); - } + indexResolver.resolveAsMergedMapping( + preAnalysis.indexPattern().indexPattern(), + result.fieldNames, + // Maybe if no indices are returned, retry without index mode and provide a clearer error message. + switch (preAnalysis.indexMode()) { + case IndexMode.TIME_SERIES -> { + var indexModeFilter = new TermQueryBuilder(IndexModeFieldMapper.NAME, IndexMode.TIME_SERIES.getName()); + yield requestFilter != null + ? new BoolQueryBuilder().filter(requestFilter).filter(indexModeFilter) + : indexModeFilter; + } + default -> requestFilter; + }, + preAnalysis.indexMode() == IndexMode.TIME_SERIES, + listener.delegateFailureAndWrap((l, indexResolution) -> { + EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, indexResolution.failures()); + l.onResponse(result.withIndices(indexResolution)); + }) + ); } else { // occurs when dealing with local relations (row a = 1) listener.onResponse(result.withIndices(IndexResolution.invalid("[none specified]"))); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtilsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtilsTests.java index 76ef94f008628..d12f7475c46c8 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtilsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtilsTests.java @@ -59,62 +59,6 @@ public class EsqlCCSUtilsTests extends ESTestCase { private final String REMOTE1_ALIAS = "remote1"; private final String REMOTE2_ALIAS = "remote2"; - public void testCreateIndexExpressionFromAvailableClusters() { - var skipped = EsqlExecutionInfo.Cluster.Status.SKIPPED; - // no clusters marked as skipped - { - EsqlExecutionInfo executionInfo = new EsqlExecutionInfo(true); - executionInfo.swapCluster(LOCAL_CLUSTER_ALIAS, (k, v) -> new EsqlExecutionInfo.Cluster(LOCAL_CLUSTER_ALIAS, "logs*", false)); - executionInfo.swapCluster(REMOTE1_ALIAS, (k, v) -> new EsqlExecutionInfo.Cluster(REMOTE1_ALIAS, "*", true)); - executionInfo.swapCluster(REMOTE2_ALIAS, (k, v) -> new EsqlExecutionInfo.Cluster(REMOTE2_ALIAS, "mylogs1,mylogs2,logs*", true)); - assertIndexPattern( - EsqlCCSUtils.createIndexExpressionFromAvailableClusters(executionInfo), - containsInAnyOrder("logs*", "remote1:*", "remote2:mylogs1", "remote2:mylogs2", "remote2:logs*") - ); - } - - // one cluster marked as skipped, so not present in revised index expression - { - EsqlExecutionInfo executionInfo = new EsqlExecutionInfo(true); - executionInfo.swapCluster(LOCAL_CLUSTER_ALIAS, (k, v) -> new EsqlExecutionInfo.Cluster(LOCAL_CLUSTER_ALIAS, "logs*", false)); - executionInfo.swapCluster(REMOTE1_ALIAS, (k, v) -> new EsqlExecutionInfo.Cluster(REMOTE1_ALIAS, "*,foo", true)); - executionInfo.swapCluster( - REMOTE2_ALIAS, - (k, v) -> new EsqlExecutionInfo.Cluster(REMOTE2_ALIAS, "mylogs1,mylogs2,logs*", true, skipped) - ); - assertIndexPattern( - EsqlCCSUtils.createIndexExpressionFromAvailableClusters(executionInfo), - containsInAnyOrder("logs*", "remote1:*", "remote1:foo") - ); - } - - // two clusters marked as skipped, so only local cluster present in revised index expression - { - EsqlExecutionInfo executionInfo = new EsqlExecutionInfo(true); - executionInfo.swapCluster(LOCAL_CLUSTER_ALIAS, (k, v) -> new EsqlExecutionInfo.Cluster(LOCAL_CLUSTER_ALIAS, "logs*", false)); - executionInfo.swapCluster(REMOTE1_ALIAS, (k, v) -> new EsqlExecutionInfo.Cluster(REMOTE1_ALIAS, "*,foo", true, skipped)); - executionInfo.swapCluster( - REMOTE2_ALIAS, - (k, v) -> new EsqlExecutionInfo.Cluster(REMOTE2_ALIAS, "mylogs1,mylogs2,logs*", true, skipped) - ); - assertThat(EsqlCCSUtils.createIndexExpressionFromAvailableClusters(executionInfo), equalTo("logs*")); - } - - // only remotes present and all marked as skipped, so in revised index expression should be empty string - { - EsqlExecutionInfo executionInfo = new EsqlExecutionInfo(true); - executionInfo.swapCluster( - REMOTE1_ALIAS, - (k, v) -> new EsqlExecutionInfo.Cluster(REMOTE1_ALIAS, "*,foo", true, EsqlExecutionInfo.Cluster.Status.SKIPPED) - ); - executionInfo.swapCluster( - REMOTE2_ALIAS, - (k, v) -> new EsqlExecutionInfo.Cluster(REMOTE2_ALIAS, "mylogs1,mylogs2,logs*", true, skipped) - ); - assertThat(EsqlCCSUtils.createIndexExpressionFromAvailableClusters(executionInfo), equalTo("")); - } - } - public void testCreateQualifiedLookupIndexExpressionFromAvailableClusters() { var skipped = EsqlExecutionInfo.Cluster.Status.SKIPPED; From 58c8230b540537faf10c51f57baaf8c7ea3751f2 Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Wed, 24 Sep 2025 13:45:36 +0200 Subject: [PATCH 06/10] assert original index expression in error --- .../qa/rest/RequestIndexFilteringTestCase.java | 4 ++-- .../remotecluster/RemoteClusterSecurityEsqlIT.java | 14 +------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java index 712f1d874e663..260844f3c25d1 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java @@ -204,12 +204,12 @@ public void testIndicesDontExist() throws IOException { ResponseException e = expectThrows(ResponseException.class, () -> runEsql(timestampFilter("gte", "2020-01-01").query(from("foo")))); assertEquals(400, e.getResponse().getStatusLine().getStatusCode()); assertThat(e.getMessage(), containsString("verification_exception")); - assertThat(e.getMessage(), anyOf(containsString("Unknown index [foo]"), containsString("Unknown index [remote_cluster:foo]"))); + assertThat(e.getMessage(), anyOf(containsString("Unknown index [foo]"), containsString("Unknown index [*:foo]"))); e = expectThrows(ResponseException.class, () -> runEsql(timestampFilter("gte", "2020-01-01").query(from("foo*")))); assertEquals(400, e.getResponse().getStatusLine().getStatusCode()); assertThat(e.getMessage(), containsString("verification_exception")); - assertThat(e.getMessage(), anyOf(containsString("Unknown index [foo*]"), containsString("Unknown index [remote_cluster:foo*]"))); + assertThat(e.getMessage(), anyOf(containsString("Unknown index [foo*]"), containsString("Unknown index [*:foo*]"))); e = expectThrows(ResponseException.class, () -> runEsql(timestampFilter("gte", "2020-01-01").query("FROM foo, test1"))); assertEquals(404, e.getResponse().getStatusLine().getStatusCode()); diff --git a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java index 2c545b54b37ef..31812ce667db1 100644 --- a/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java +++ b/x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityEsqlIT.java @@ -42,12 +42,9 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -986,16 +983,7 @@ public void testAlias() throws Exception { Request request = esqlRequest("FROM " + index + " | KEEP emp_id | SORT emp_id | LIMIT 100"); ResponseException error = expectThrows(ResponseException.class, () -> performRequestWithRemoteSearchUser(request)); assertThat(error.getResponse().getStatusLine().getStatusCode(), equalTo(400)); - String expectedIndexExpressionInError = index.replace("*", "my_remote_cluster"); - Pattern p = Pattern.compile("Unknown index \\[([^\\]]+)\\]"); - Matcher m = p.matcher(error.getMessage()); - assertTrue("Pattern matcher to parse error message did not find matching string: " + error.getMessage(), m.find()); - String unknownIndexExpressionInErrorMessage = m.group(1); - Set actualUnknownIndexes = org.elasticsearch.common.Strings.commaDelimitedListToSet( - unknownIndexExpressionInErrorMessage - ); - Set expectedUnknownIndexes = org.elasticsearch.common.Strings.commaDelimitedListToSet(expectedIndexExpressionInError); - assertThat(actualUnknownIndexes, equalTo(expectedUnknownIndexes)); + assertThat(error.getMessage(), containsString("Unknown index [" + index + "]")); } for (var index : List.of( From ee777a4c2a821ab831678df52fcb3e08c67de3c6 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 24 Sep 2025 11:52:44 +0000 Subject: [PATCH 07/10] [CI] Update transport version definitions --- server/src/main/resources/transport/upper_bounds/9.2.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/resources/transport/upper_bounds/9.2.csv b/server/src/main/resources/transport/upper_bounds/9.2.csv index 6e7d51d3d3020..b1209b927d8a5 100644 --- a/server/src/main/resources/transport/upper_bounds/9.2.csv +++ b/server/src/main/resources/transport/upper_bounds/9.2.csv @@ -1 +1 @@ -security_stats_endpoint,9168000 +inference_api_openai_embeddings_headers,9169000 From 06bfcfa6a6667e2f208072c21631741450cc4e13 Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Wed, 24 Sep 2025 15:40:40 +0200 Subject: [PATCH 08/10] upd --- .../indices/IndicesExpressionGrouper.java | 35 +-------------- .../transport/RemoteClusterService.java | 6 +-- ...ossClusterQueriesWithInvalidLicenseIT.java | 39 ++++++---------- .../xpack/esql/session/EsqlCCSUtils.java | 14 ++---- .../xpack/esql/session/EsqlSession.java | 45 +++++++++++-------- .../xpack/esql/session/EsqlCCSUtilsTests.java | 6 +-- .../telemetry/PlanExecutorMetricsTests.java | 2 +- 7 files changed, 49 insertions(+), 98 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/indices/IndicesExpressionGrouper.java b/server/src/main/java/org/elasticsearch/indices/IndicesExpressionGrouper.java index 9660c57260529..7f3d1cb6bedbb 100644 --- a/server/src/main/java/org/elasticsearch/indices/IndicesExpressionGrouper.java +++ b/server/src/main/java/org/elasticsearch/indices/IndicesExpressionGrouper.java @@ -11,10 +11,8 @@ import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.support.IndicesOptions; -import org.elasticsearch.common.Strings; import java.util.Map; -import java.util.Set; /** * Interface for grouping index expressions, along with IndicesOptions by cluster alias. @@ -30,36 +28,7 @@ public interface IndicesExpressionGrouper { /** - * @param remoteClusterNames Set of configured remote cluster names. - * @param indicesOptions IndicesOptions to clarify how the index expression should be parsed/applied - * @param indexExpressionCsv Multiple index expressions as CSV string (with no spaces), e.g., "logs1,logs2,cluster-a:logs1". - * A single index expression is also supported. - * @return Map where the key is the cluster alias (for "local" cluster, it is RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY) - * and the value for that cluster from the index expression is an OriginalIndices object. + * See {@link org.elasticsearch.transport.RemoteClusterService#groupIndices} for details */ - default Map groupIndices( - Set remoteClusterNames, - IndicesOptions indicesOptions, - String indexExpressionCsv - ) { - return groupIndices(remoteClusterNames, indicesOptions, Strings.splitStringByCommaToArray(indexExpressionCsv)); - } - - /** - * Same behavior as the other groupIndices, except the incoming multiple index expressions must already be - * parsed into a String array. - * @param remoteClusterNames Set of configured remote cluster names. - * @param indicesOptions IndicesOptions to clarify how the index expressions should be parsed/applied - * @param indexExpressions Multiple index expressions as string[]. - * @return Map where the key is the cluster alias (for "local" cluster, it is RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY) - * and the value for that cluster from the index expression is an OriginalIndices object. - */ - Map groupIndices(Set remoteClusterNames, IndicesOptions indicesOptions, String[] indexExpressions); - - /** - * Returns a set of currently configured remote clusters. - */ - default Set getConfiguredClusters() { - return Set.of(); - } + Map groupIndices(IndicesOptions indicesOptions, String[] indexExpressions, boolean returnLocalAll); } diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java index d2ad9f95f23e7..419aa189e352c 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterService.java @@ -168,6 +168,7 @@ public Map groupIndices(Set remoteClusterNames, return groupIndices(remoteClusterNames, indicesOptions, indices, true); } + @Override public Map groupIndices(IndicesOptions indicesOptions, String[] indices, boolean returnLocalAll) { return groupIndices(getRegisteredRemoteClusterNames(), indicesOptions, indices, returnLocalAll); } @@ -176,11 +177,6 @@ public Map groupIndices(IndicesOptions indicesOptions, return groupIndices(getRegisteredRemoteClusterNames(), indicesOptions, indices, true); } - @Override - public Set getConfiguredClusters() { - return getRegisteredRemoteClusterNames(); - } - /** * Returns the registered remote cluster names. */ diff --git a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java index 517f3e1fd5860..a2e082e07579e 100644 --- a/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java +++ b/x-pack/plugin/esql/src/internalClusterTest/java/org/elasticsearch/xpack/esql/action/CrossClusterQueriesWithInvalidLicenseIT.java @@ -10,7 +10,6 @@ import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.core.Tuple; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.plan.logical.Enrich; import java.util.ArrayList; @@ -66,30 +65,20 @@ public void testQueryAgainstNonMatchingClusterWildcardPattern() { // since this wildcarded expression does not resolve to a valid remote cluster, it is not considered // a cross-cluster search and thus should not throw a license error - expectThrows( - VerificationException.class, - containsString("Unknown index [xremote*:events]"), - () -> runQuery("FROM xremote*:events | STATS count(*)", requestIncludeMeta).close() - ); - // try (EsqlQueryResponse resp = runQuery("FROM xremote*:events | STATS count(*)", requestIncludeMeta)) { - // assertThat(resp.columns().size(), equalTo(1)); - // EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); - // assertThat(executionInfo.isCrossClusterSearch(), is(false)); - // assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); - // } - - expectThrows( - VerificationException.class, - containsString("Unknown index [xremote*:events]"), - () -> runQuery("FROM xremote*:events | STATS count(*)", requestIncludeMeta).close() - ); - // try (EsqlQueryResponse resp = runQuery("FROM xremote*:events | LIMIT 0", requestIncludeMeta)) { - // assertThat(resp.columns().size(), equalTo(1)); - // assertThat(getValuesList(resp).size(), equalTo(0)); - // EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); - // assertThat(executionInfo.isCrossClusterSearch(), is(false)); - // assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); - // } + try (EsqlQueryResponse resp = runQuery("FROM xremote*:events | STATS count(*)", requestIncludeMeta)) { + assertThat(resp.columns().size(), equalTo(1)); + EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); + assertThat(executionInfo.isCrossClusterSearch(), is(false)); + assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); + } + + try (EsqlQueryResponse resp = runQuery("FROM xremote*:events | LIMIT 0", requestIncludeMeta)) { + assertThat(resp.columns().size(), equalTo(1)); + assertThat(getValuesList(resp).size(), equalTo(0)); + EsqlExecutionInfo executionInfo = resp.getExecutionInfo(); + assertThat(executionInfo.isCrossClusterSearch(), is(false)); + assertThat(executionInfo.includeCCSMetadata(), equalTo(responseExpectMeta)); + } } public void testCCSWithLimit0() { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtils.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtils.java index d0aaac39e8cfc..75b47271581ce 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtils.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtils.java @@ -23,7 +23,6 @@ import org.elasticsearch.transport.ConnectTransportException; import org.elasticsearch.transport.NoSuchRemoteClusterException; import org.elasticsearch.transport.RemoteClusterAware; -import org.elasticsearch.transport.RemoteClusterService; import org.elasticsearch.transport.RemoteTransportException; import org.elasticsearch.xpack.esql.VerificationException; import org.elasticsearch.xpack.esql.action.EsqlExecutionInfo; @@ -316,11 +315,9 @@ public static void initCrossClusterState( } try { var groupedIndices = indicesGrouper.groupIndices( - // indicesGrouper.getConfiguredClusters() might return mutable set that changes as clusters connect or disconnect. - // it is copied here so that we have the same resolution when request contains multiple remote cluster patterns with * - Set.copyOf(indicesGrouper.getConfiguredClusters()), IndicesOptions.DEFAULT, - indexPattern.indexPattern() + Strings.splitStringByCommaToArray(indexPattern.indexPattern()), + false ); executionInfo.clusterInfoInitializing(true); @@ -339,11 +336,8 @@ public static void initCrossClusterState( executionInfo.clusterInfoInitializing(false); } - // check if it is a cross-cluster query - if (groupedIndices.size() > 1 || groupedIndices.containsKey(RemoteClusterService.LOCAL_CLUSTER_GROUP_KEY) == false) { - if (EsqlLicenseChecker.isCcsAllowed(licenseState) == false) { - throw EsqlLicenseChecker.invalidLicenseForCcsException(licenseState); - } + if (executionInfo.isCrossClusterSearch() && EsqlLicenseChecker.isCcsAllowed(licenseState) == false) { + throw EsqlLicenseChecker.invalidLicenseForCcsException(licenseState); } } catch (NoSuchRemoteClusterException e) { if (EsqlLicenseChecker.isCcsAllowed(licenseState)) { diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java index 0990199adf6f2..34ec46a0788d5 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java @@ -647,25 +647,32 @@ private void preAnalyzeMainIndices( ThreadPool.Names.SYSTEM_READ ); if (preAnalysis.indexPattern() != null) { - indexResolver.resolveAsMergedMapping( - preAnalysis.indexPattern().indexPattern(), - result.fieldNames, - // Maybe if no indices are returned, retry without index mode and provide a clearer error message. - switch (preAnalysis.indexMode()) { - case IndexMode.TIME_SERIES -> { - var indexModeFilter = new TermQueryBuilder(IndexModeFieldMapper.NAME, IndexMode.TIME_SERIES.getName()); - yield requestFilter != null - ? new BoolQueryBuilder().filter(requestFilter).filter(indexModeFilter) - : indexModeFilter; - } - default -> requestFilter; - }, - preAnalysis.indexMode() == IndexMode.TIME_SERIES, - listener.delegateFailureAndWrap((l, indexResolution) -> { - EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, indexResolution.failures()); - l.onResponse(result.withIndices(indexResolution)); - }) - ); + if (executionInfo.clusterAliases().isEmpty()) { + // if this was a pure remote CCS request (no local indices) and all remotes are offline, return an empty IndexResolution + listener.onResponse( + result.withIndices(IndexResolution.valid(new EsIndex(preAnalysis.indexPattern().indexPattern(), Map.of(), Map.of()))) + ); + } else { + indexResolver.resolveAsMergedMapping( + preAnalysis.indexPattern().indexPattern(), + result.fieldNames, + // Maybe if no indices are returned, retry without index mode and provide a clearer error message. + switch (preAnalysis.indexMode()) { + case IndexMode.TIME_SERIES -> { + var indexModeFilter = new TermQueryBuilder(IndexModeFieldMapper.NAME, IndexMode.TIME_SERIES.getName()); + yield requestFilter != null + ? new BoolQueryBuilder().filter(requestFilter).filter(indexModeFilter) + : indexModeFilter; + } + default -> requestFilter; + }, + preAnalysis.indexMode() == IndexMode.TIME_SERIES, + listener.delegateFailureAndWrap((l, indexResolution) -> { + EsqlCCSUtils.updateExecutionInfoWithUnavailableClusters(executionInfo, indexResolution.failures()); + l.onResponse(result.withIndices(indexResolution)); + }) + ); + } } else { // occurs when dealing with local relations (row a = 1) listener.onResponse(result.withIndices(IndexResolution.invalid("[none specified]"))); diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtilsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtilsTests.java index d12f7475c46c8..3a2eaa544c6ab 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtilsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/session/EsqlCCSUtilsTests.java @@ -743,11 +743,7 @@ private XPackLicenseStatus inactiveLicenseStatus(License.OperationMode operation static class TestIndicesExpressionGrouper implements IndicesExpressionGrouper { @Override - public Map groupIndices( - Set remoteClusterNames, - IndicesOptions indicesOptions, - String[] indexExpressions - ) { + public Map groupIndices(IndicesOptions indicesOptions, String[] indexExpressions, boolean returnLocalAll) { final Map originalIndicesMap = new HashMap<>(); final String localKey = RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY; diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/telemetry/PlanExecutorMetricsTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/telemetry/PlanExecutorMetricsTests.java index 752e61c240cd5..c24ae185c5f25 100644 --- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/telemetry/PlanExecutorMetricsTests.java +++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/telemetry/PlanExecutorMetricsTests.java @@ -157,7 +157,7 @@ public void testFailedMetric() { // test a failed query: xyz field doesn't exist request.query("from test | stats m = max(xyz)"); EsqlSession.PlanRunner runPhase = (p, r) -> fail("this shouldn't happen"); - IndicesExpressionGrouper groupIndicesByCluster = (remoteClusterNames, indicesOptions, indexExpressions) -> Map.of( + IndicesExpressionGrouper groupIndicesByCluster = (indicesOptions, indexExpressions, returnLocalAll) -> Map.of( "", new OriginalIndices(new String[] { "test" }, IndicesOptions.DEFAULT) ); From cf3111546461678d45c9166be228cbabcadd8e8b Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Wed, 24 Sep 2025 16:47:01 +0200 Subject: [PATCH 09/10] fix flakiness --- .../qa/rest/RequestIndexFilteringTestCase.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java index 260844f3c25d1..addbb6985818c 100644 --- a/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java +++ b/x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/RequestIndexFilteringTestCase.java @@ -204,12 +204,26 @@ public void testIndicesDontExist() throws IOException { ResponseException e = expectThrows(ResponseException.class, () -> runEsql(timestampFilter("gte", "2020-01-01").query(from("foo")))); assertEquals(400, e.getResponse().getStatusLine().getStatusCode()); assertThat(e.getMessage(), containsString("verification_exception")); - assertThat(e.getMessage(), anyOf(containsString("Unknown index [foo]"), containsString("Unknown index [*:foo]"))); + assertThat( + e.getMessage(), + anyOf( + containsString("Unknown index [foo]"), + containsString("Unknown index [*:foo]"), + containsString("Unknown index [remote_cluster:foo]") + ) + ); e = expectThrows(ResponseException.class, () -> runEsql(timestampFilter("gte", "2020-01-01").query(from("foo*")))); assertEquals(400, e.getResponse().getStatusLine().getStatusCode()); assertThat(e.getMessage(), containsString("verification_exception")); - assertThat(e.getMessage(), anyOf(containsString("Unknown index [foo*]"), containsString("Unknown index [*:foo*]"))); + assertThat( + e.getMessage(), + anyOf( + containsString("Unknown index [foo*]"), + containsString("Unknown index [*:foo*]"), + containsString("Unknown index [remote_cluster:foo*]") + ) + ); e = expectThrows(ResponseException.class, () -> runEsql(timestampFilter("gte", "2020-01-01").query("FROM foo, test1"))); assertEquals(404, e.getResponse().getStatusLine().getStatusCode()); From e5254ec251b1a6f9ef3237ce99ca0979171f9771 Mon Sep 17 00:00:00 2001 From: "ievgen.degtiarenko" Date: Wed, 1 Oct 2025 15:36:01 +0200 Subject: [PATCH 10/10] update comment --- .../java/org/elasticsearch/xpack/esql/session/EsqlSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java index d127e04523248..d1b49b52c934e 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/session/EsqlSession.java @@ -694,7 +694,7 @@ private void preAnalyzeMainIndices( ); if (preAnalysis.indexPattern() != null) { if (executionInfo.clusterAliases().isEmpty()) { - // if this was a pure remote CCS request (no local indices) and all remotes are offline, return an empty IndexResolution + // return empty resolution if the expression is pure CCS and resolved no remote clusters (like no-such-cluster*:index) listener.onResponse( result.withIndices(IndexResolution.valid(new EsIndex(preAnalysis.indexPattern().indexPattern(), Map.of(), Map.of()))) );