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 @@ -33,6 +33,7 @@
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.indices.SystemIndices;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
import org.elasticsearch.telemetry.metric.MeterRegistry;
import org.elasticsearch.threadpool.DefaultBuiltInExecutorBuilders;
import org.elasticsearch.threadpool.ThreadPool;
Expand Down Expand Up @@ -342,7 +343,7 @@ static class BenchmarkViewResolver extends ViewResolver {
boolean enabled,
ViewResolutionService viewResolutionService
) {
super(clusterService, projectResolver, null);
super(clusterService, projectResolver, null, CrossProjectModeDecider.NOOP);
this.enabled = enabled;
this.viewResolutionService = viewResolutionService;
this.benchmarkClusterService = clusterService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,14 @@ public Collection<?> createComponents(PluginServices services) {
);
if (ESQL_VIEWS_FEATURE_FLAG.isEnabled()) {
components = new ArrayList<>(components);
components.add(new ViewResolver(services.clusterService(), services.projectResolver(), services.client()));
components.add(
new ViewResolver(
services.clusterService(),
services.projectResolver(),
services.client(),
services.crossProjectModeDecider()
)
);
components.add(new ViewService(services.clusterService(), parser));
}
return components;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.elasticsearch.index.IndexMode;
import org.elasticsearch.logging.LogManager;
import org.elasticsearch.logging.Logger;
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
import org.elasticsearch.xpack.core.esql.EsqlFeatureFlags;
import org.elasticsearch.xpack.esql.VerificationException;
import org.elasticsearch.xpack.esql.action.EsqlResolveViewAction;
Expand Down Expand Up @@ -51,6 +52,7 @@ public class ViewResolver {
protected Logger log = LogManager.getLogger(getClass());
private final ClusterService clusterService;
private final ProjectResolver projectResolver;
private final CrossProjectModeDecider crossProjectModeDecider;
private volatile int maxViewDepth;
private final Client client;
public static final Setting<Integer> MAX_VIEW_DEPTH_SETTING = Setting.intSetting(
Expand All @@ -68,13 +70,20 @@ public class ViewResolver {
public ViewResolver() {
this.clusterService = null;
this.projectResolver = null;
this.crossProjectModeDecider = CrossProjectModeDecider.NOOP;
this.maxViewDepth = 0;
this.client = null;
}

public ViewResolver(ClusterService clusterService, ProjectResolver projectResolver, Client client) {
public ViewResolver(
ClusterService clusterService,
ProjectResolver projectResolver,
Client client,
CrossProjectModeDecider crossProjectModeDecider
) {
this.clusterService = clusterService;
this.projectResolver = projectResolver;
this.crossProjectModeDecider = crossProjectModeDecider;
this.client = client;
clusterService.getClusterSettings().initializeAndWatch(MAX_VIEW_DEPTH_SETTING, v -> this.maxViewDepth = v);
}
Expand Down Expand Up @@ -310,8 +319,11 @@ private List<String> buildUnresolvedPatterns(
unresolvedPatterns.add(resolvedIndexExpression.original());
continue;
}
// If any of the concrete resources were not views, pass them along as an unresolved relation
if (resolvedIndexExpression.localExpressions().indices().stream().anyMatch(index -> seenViews.contains(index) == false)) {
// If any of the concrete resources were not views, pass them along as an unresolved relation.
// When CPS is enabled, also keep wildcard patterns because they may match remote indexes
// in other projects (unlike CCS, CPS does not require explicit remote references).
if (resolvedIndexExpression.localExpressions().indices().stream().anyMatch(index -> seenViews.contains(index) == false)
|| (crossProjectModeDecider.crossProjectEnabled() && Regex.isSimpleMatchPattern(resolvedIndexExpression.original()))) {
unresolvedPatterns.add(resolvedIndexExpression.original());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.indices.EmptySystemIndices;
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.esql.action.EsqlResolveViewAction;
Expand All @@ -32,8 +33,12 @@ public class InMemoryViewResolver extends ViewResolver {
protected ClusterService clusterService;
protected ProjectResolver projectResolver;

public InMemoryViewResolver(ClusterService clusterService, Supplier<ViewMetadata> metadata) {
super(clusterService, null, null);
public InMemoryViewResolver(
ClusterService clusterService,
Supplier<ViewMetadata> metadata,
CrossProjectModeDecider crossProjectModeDecider
) {
super(clusterService, null, null, crossProjectModeDecider);
this.projectResolver = DefaultProjectResolver.INSTANCE;
this.indexNameExpressionResolver = new IndexNameExpressionResolver(
new ThreadContext(Settings.EMPTY),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexVersion;
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
import org.elasticsearch.test.ClusterServiceUtils;
import org.elasticsearch.threadpool.TestThreadPool;
import org.elasticsearch.threadpool.ThreadPool;
Expand Down Expand Up @@ -161,6 +162,10 @@ void clearAllViewsAndIndices() {
}

public InMemoryViewResolver getViewResolver() {
return new InMemoryViewResolver(clusterService, () -> viewMetadata);
return new InMemoryViewResolver(clusterService, () -> viewMetadata, CrossProjectModeDecider.NOOP);
}

public InMemoryViewResolver getViewResolver(CrossProjectModeDecider crossProjectModeDecider) {
return new InMemoryViewResolver(clusterService, () -> viewMetadata, crossProjectModeDecider);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.elasticsearch.cluster.metadata.View;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
import org.elasticsearch.xpack.esql.ConfigurationTestUtils;
import org.elasticsearch.xpack.esql.SerializationTestUtils;
import org.elasticsearch.xpack.esql.VerificationException;
Expand Down Expand Up @@ -1060,12 +1061,104 @@ public void testIndexCompactionWithNestedNamedSubqueries() {
}
}

/**
* When CPS is enabled and a wildcard matches only views (no local indexes), the wildcard is still preserved
* as an unresolved pattern because remote projects may have matching indexes.
*/
public void testCPSWildcardPreservedWhenOnlyViewsMatch() {
addView("view1", "FROM emp1");
addView("view2", "FROM emp2");
addView("view3", "FROM emp3");
LogicalPlan plan = query("FROM view*");
// Without CPS: wildcard is fully replaced (no unresolved pattern)
assertThat(replaceViews(plan), matchesPlan(query("FROM emp1, emp2, emp3")));
// With CPS: wildcard is preserved alongside the resolved views
assertThat(replaceViewsWithCPS(plan), matchesPlan(query("FROM emp1, emp2, emp3, view*")));
}

/**
* When CPS is enabled and a wildcard matches views with pipe bodies (no local indexes), the wildcard is preserved.
*/
public void testCPSWildcardPreservedWithPipeBodiesWhenOnlyViewsMatch() {
addView("view_1", "FROM emp1 | WHERE emp.age > 30");
addView("view_2", "FROM emp2 | WHERE emp.age < 40");
addView("view_3", "FROM emp3 | WHERE emp.salary > 50000");
LogicalPlan plan = query("FROM view*");
// Without CPS: 3 subqueries (just the views)
LogicalPlan withoutCPS = replaceViews(plan);
assertThat(withoutCPS, instanceOf(ViewUnionAll.class));
assertThat(withoutCPS.children().size(), equalTo(3));
// With CPS: 4 subqueries (3 views + the preserved wildcard)
LogicalPlan withCPS = replaceViewsWithCPS(plan);
assertThat(withCPS, instanceOf(ViewUnionAll.class));
assertThat(withCPS.children().size(), equalTo(4));
assertThat(
withCPS.children(),
containsInAnyOrder(
matchesPlan(query("FROM view*")),
matchesPlan(query("FROM emp1 | WHERE emp.age > 30")),
matchesPlan(query("FROM emp2 | WHERE emp.age < 40")),
matchesPlan(query("FROM emp3 | WHERE emp.salary > 50000"))
)
);
}

/**
* When CPS is enabled and a wildcard already matches a local index alongside views, the wildcard is preserved
* (same as without CPS in this case).
*/
public void testCPSWildcardWithIndexMatchBehavesLikeNonCPS() {
addIndex("viewX");
addView("view1", "FROM emp1");
addView("view2", "FROM emp2");
addView("view3", "FROM emp3");
LogicalPlan plan = query("FROM view*");
// Both should preserve the wildcard since there's a matching local index
assertThat(replaceViews(plan), matchesPlan(query("FROM emp1,emp2,emp3,view*")));
assertThat(replaceViewsWithCPS(plan), matchesPlan(query("FROM emp1,emp2,emp3,view*")));
}

/**
* CPS does not affect concrete (non-wildcard) view references — they are still fully replaced.
* This is because we separately report the view names to the index resolution layer for CPS anyway.
*/
public void testCPSConcreteViewFullyReplaced() {
addView("view1", "FROM emp1");
LogicalPlan plan = query("FROM view1");
assertThat(replaceViews(plan), matchesPlan(query("FROM emp1")));
assertThat(replaceViewsWithCPS(plan), matchesPlan(query("FROM emp1")));
}

/**
* When CPS is enabled and nested views use wildcards that match only views, wildcards are preserved.
*/
public void testCPSNestedWildcardPreserved() {
addView("view_1", "FROM emp1");
addView("view_2", "FROM emp2");
addView("view_3", "FROM emp3");
addView("view_1_2", "FROM view_1, view_2");
addView("view_1_3", "FROM view_1, view_3");
LogicalPlan plan = query("FROM view_1_*");
// Without CPS: wildcard fully replaced
assertThat(replaceViews(plan), matchesPlan(query("FROM emp1,emp3,emp1,emp2")));
// With CPS: wildcard preserved
assertThat(replaceViewsWithCPS(plan), matchesPlan(query("FROM emp1,emp3,emp1,emp2,view_1_*")));
}

private LogicalPlan replaceViews(LogicalPlan plan) {
PlainActionFuture<ViewResolver.ViewResolutionResult> future = new PlainActionFuture<>();
viewResolver.replaceViews(plan, this::parse, future);
return future.actionGet().plan();
}

private LogicalPlan replaceViewsWithCPS(LogicalPlan plan) {
var cpsDecider = new CrossProjectModeDecider(Settings.builder().put("serverless.cross_project.enabled", true).build());
InMemoryViewResolver cpsResolver = viewService.getViewResolver(cpsDecider);
PlainActionFuture<ViewResolver.ViewResolutionResult> future = new PlainActionFuture<>();
cpsResolver.replaceViews(plan, this::parse, future);
return future.actionGet().plan();
}

private void addIndex(String name) {
viewService.addIndex(projectId, name);
}
Expand Down
Loading