diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/PreAnalyzer.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/PreAnalyzer.java index 6b4570dbb2f6a..90b0124862231 100644 --- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/PreAnalyzer.java +++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/PreAnalyzer.java @@ -15,31 +15,15 @@ import org.elasticsearch.xpack.esql.plan.logical.UnresolvedRelation; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; - -import static java.util.Collections.emptyList; /** * This class is part of the planner. Acts somewhat like a linker, to find the indices and enrich policies referenced by the query. */ public class PreAnalyzer { - public static class PreAnalysis { - public static final PreAnalysis EMPTY = new PreAnalysis(null, emptyList(), emptyList(), emptyList()); - - public final IndexMode indexMode; - public final List indices; - public final List enriches; - public final List lookupIndices; - - public PreAnalysis(IndexMode indexMode, List indices, List enriches, List lookupIndices) { - this.indexMode = indexMode; - this.indices = indices; - this.enriches = enriches; - this.lookupIndices = lookupIndices; - } + public record PreAnalysis(IndexMode indexMode, IndexPattern index, List enriches, List lookupIndices) { + public static final PreAnalysis EMPTY = new PreAnalysis(null, null, List.of(), List.of()); } public PreAnalysis preAnalyze(LogicalPlan plan) { @@ -51,18 +35,19 @@ public PreAnalysis preAnalyze(LogicalPlan plan) { } protected PreAnalysis doPreAnalyze(LogicalPlan plan) { - Set indices = new HashSet<>(); + + Holder indexMode = new Holder<>(); + Holder index = new Holder<>(); List unresolvedEnriches = new ArrayList<>(); List lookupIndices = new ArrayList<>(); - Holder indexMode = new Holder<>(); plan.forEachUp(UnresolvedRelation.class, p -> { if (p.indexMode() == IndexMode.LOOKUP) { lookupIndices.add(p.indexPattern()); } else if (indexMode.get() == null || indexMode.get() == p.indexMode()) { indexMode.set(p.indexMode()); - indices.add(p.indexPattern()); + index.set(p.indexPattern()); } else { throw new IllegalStateException("index mode is already set"); } @@ -73,7 +58,6 @@ protected PreAnalysis doPreAnalyze(LogicalPlan plan) { // mark plan as preAnalyzed (if it were marked, there would be no analysis) plan.forEachUp(LogicalPlan::setPreAnalyzed); - return new PreAnalysis(indexMode.get(), indices.stream().toList(), unresolvedEnriches, lookupIndices); + return new PreAnalysis(indexMode.get(), index.get(), unresolvedEnriches, lookupIndices); } - } 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 a4a8ed63a1a03..a86c0414468ad 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 @@ -322,20 +322,19 @@ static void updateExecutionInfoAtEndOfPlanning(EsqlExecutionInfo execInfo) { public static void initCrossClusterState( IndicesExpressionGrouper indicesGrouper, XPackLicenseState licenseState, - List patterns, + IndexPattern pattern, EsqlExecutionInfo executionInfo ) throws ElasticsearchStatusException { - if (patterns.isEmpty()) { + if (pattern == null) { return; } - assert patterns.size() == 1 : "Only single index pattern is supported"; 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, - patterns.getFirst().indexPattern() + pattern.indexPattern() ); executionInfo.clusterInfoInitializing(true); 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..a8078feec4f8b 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 @@ -50,7 +50,6 @@ import org.elasticsearch.xpack.esql.expression.function.EsqlFunctionRegistry; import org.elasticsearch.xpack.esql.index.EsIndex; import org.elasticsearch.xpack.esql.index.IndexResolution; -import org.elasticsearch.xpack.esql.index.MappingException; import org.elasticsearch.xpack.esql.inference.InferenceResolution; import org.elasticsearch.xpack.esql.inference.InferenceService; import org.elasticsearch.xpack.esql.optimizer.LogicalPlanOptimizer; @@ -374,14 +373,14 @@ public void analyzedPlan( } var preAnalysis = preAnalyzer.preAnalyze(parsed); - EsqlCCSUtils.initCrossClusterState(indicesExpressionGrouper, verifier.licenseState(), preAnalysis.indices, executionInfo); + EsqlCCSUtils.initCrossClusterState(indicesExpressionGrouper, verifier.licenseState(), preAnalysis.index(), executionInfo); SubscribableListener. // - newForked(l -> enrichPolicyResolver.resolvePolicies(preAnalysis.enriches, executionInfo, l)) + newForked(l -> enrichPolicyResolver.resolvePolicies(preAnalysis.enriches(), executionInfo, l)) .andThenApply(enrichResolution -> FieldNameUtils.resolveFieldNames(parsed, enrichResolution)) .andThen((l, r) -> resolveInferences(parsed, r, l)) .andThen((l, r) -> preAnalyzeMainIndices(preAnalysis, executionInfo, r, requestFilter, l)) - .andThen((l, r) -> preAnalyzeLookupIndices(preAnalysis.lookupIndices.iterator(), r, executionInfo, l)) + .andThen((l, r) -> preAnalyzeLookupIndices(preAnalysis.lookupIndices().iterator(), r, executionInfo, l)) .andThen((l, r) -> analyzeWithRetry(parsed, requestFilter, preAnalysis, executionInfo, r, l)) .addListener(logicalPlanListener); } @@ -633,26 +632,17 @@ 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) + if (preAnalysis.index() != 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.withIndexResolution(IndexResolution.valid(new EsIndex(table.indexPattern(), Map.of(), Map.of()))) + result.withIndexResolution(IndexResolution.valid(new EsIndex(preAnalysis.index().indexPattern(), Map.of(), Map.of()))) ); } else { boolean includeAllDimensions = false; // call the EsqlResolveFieldsAction (field-caps) to resolve indices and get field types - if (preAnalysis.indexMode == IndexMode.TIME_SERIES) { + 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()); @@ -673,12 +663,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); - } + // occurs when dealing with local relations (row a = 1) + listener.onResponse(result.withIndexResolution(IndexResolution.invalid("[none specified]"))); } } 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 2941355eac39c..2ff6ce71be516 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,18 +530,12 @@ private LogicalPlan analyzedPlan(LogicalPlan parsed, CsvTestsDataLoader.MultiInd private static CsvTestsDataLoader.MultiIndexTestDataset testDatasets(LogicalPlan parsed) { var preAnalysis = new PreAnalyzer().preAnalyze(parsed); - var indices = preAnalysis.indices; - if (indices.isEmpty()) { - /* - * If the data set doesn't matter we'll just grab one we know works. - * Employees is fine. - */ + if (preAnalysis.index() == 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")); - } else if (preAnalysis.indices.size() > 1) { - throw new IllegalArgumentException("unexpected index resolution to multiple entries [" + preAnalysis.indices.size() + "]"); } - String indexName = indices.getFirst().indexPattern(); + String indexName = preAnalysis.index().indexPattern(); List datasets = new ArrayList<>(); if (indexName.endsWith("*")) { String indexPrefix = indexName.substring(0, indexName.length() - 1); 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 a12d26f48b608..579c30c4badae 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 @@ -699,7 +699,7 @@ public void testInitCrossClusterState() { // local only search works with any license state { - var localOnly = List.of(new IndexPattern(EMPTY, randomFrom("idx", "idx1,idx2*"))); + var localOnly = new IndexPattern(EMPTY, randomFrom("idx", "idx1,idx2*")); assertLicenseCheckPasses(indicesGrouper, null, localOnly, ""); for (var mode : License.OperationMode.values()) { @@ -710,7 +710,7 @@ public void testInitCrossClusterState() { // cross-cluster search requires a valid (active, non-expired) enterprise license OR a valid trial license { - var remote = List.of(new IndexPattern(EMPTY, randomFrom("idx,remote:idx", "idx1,remote:idx2*,remote:logs"))); + var remote = new IndexPattern(EMPTY, randomFrom("idx,remote:idx", "idx1,remote:idx2*,remote:logs")); var supportedLicenses = EnumSet.of(License.OperationMode.TRIAL, License.OperationMode.ENTERPRISE); var unsupportedLicenses = EnumSet.complementOf(supportedLicenses); @@ -738,18 +738,18 @@ private static XPackLicenseState createLicenseState(XPackLicenseStatus status) { private void assertLicenseCheckPasses( TestIndicesExpressionGrouper indicesGrouper, XPackLicenseStatus status, - List patterns, + IndexPattern pattern, String... expectedRemotes ) { var executionInfo = new EsqlExecutionInfo(true); - initCrossClusterState(indicesGrouper, createLicenseState(status), patterns, executionInfo); + initCrossClusterState(indicesGrouper, createLicenseState(status), pattern, executionInfo); assertThat(executionInfo.clusterAliases(), containsInAnyOrder(expectedRemotes)); } private void assertLicenseCheckFails( TestIndicesExpressionGrouper indicesGrouper, XPackLicenseStatus licenseStatus, - List patterns, + IndexPattern pattern, String expectedErrorMessageSuffix ) { ElasticsearchStatusException e = expectThrows( @@ -757,7 +757,7 @@ private void assertLicenseCheckFails( equalTo( "A valid Enterprise license is required to run ES|QL cross-cluster searches. License found: " + expectedErrorMessageSuffix ), - () -> initCrossClusterState(indicesGrouper, createLicenseState(licenseStatus), patterns, new EsqlExecutionInfo(true)) + () -> initCrossClusterState(indicesGrouper, createLicenseState(licenseStatus), pattern, new EsqlExecutionInfo(true)) ); assertThat(e.status(), equalTo(RestStatus.BAD_REQUEST)); }