Skip to content

Commit 72d4501

Browse files
committed
Expand REST API to cover get and list to make it easier to check cluster status before loading views
1 parent 6444bee commit 72d4501

File tree

13 files changed

+538
-43
lines changed

13 files changed

+538
-43
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"esql.get_view":{
3+
"documentation":{
4+
"url": "https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-esql-view-get",
5+
"description":"Get a VIEW for ESQL."
6+
},
7+
"stability": "experimental",
8+
"visibility": "public",
9+
"headers": {
10+
"accept": [
11+
"application/json"
12+
],
13+
"content_type": [
14+
"application/json"
15+
]
16+
},
17+
"url":{
18+
"paths":[
19+
{
20+
"path":"/_query/view/{name}",
21+
"methods":[
22+
"GET"
23+
],
24+
"parts": {
25+
"name": {
26+
"type": "string",
27+
"description": "The name of the view"
28+
}
29+
}
30+
}
31+
]
32+
}
33+
}
34+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"esql.list_views": {
3+
"documentation": {
4+
"url": null,
5+
"description": "List all defined ES|QL views"
6+
},
7+
"stability": "experimental",
8+
"visibility": "public",
9+
"headers": {
10+
"accept": [
11+
"application/json"
12+
],
13+
"content_type": [
14+
"application/json"
15+
]
16+
},
17+
"url": {
18+
"paths": [
19+
{
20+
"path": "/_query/views",
21+
"methods": [
22+
"GET"
23+
]
24+
}
25+
]
26+
}
27+
}
28+
}

x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/EsqlSpecTestCase.java

Lines changed: 58 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@
7171
import static org.elasticsearch.xpack.esql.CsvTestUtils.loadCsvSpecValues;
7272
import static org.elasticsearch.xpack.esql.CsvTestsDataLoader.createInferenceEndpoints;
7373
import static org.elasticsearch.xpack.esql.CsvTestsDataLoader.deleteInferenceEndpoints;
74+
import static org.elasticsearch.xpack.esql.CsvTestsDataLoader.deleteViews;
7475
import static org.elasticsearch.xpack.esql.CsvTestsDataLoader.loadDataSetIntoEs;
76+
import static org.elasticsearch.xpack.esql.CsvTestsDataLoader.loadViewsIntoEs;
7577
import static org.elasticsearch.xpack.esql.EsqlTestUtils.classpathResources;
7678
import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.COMPLETION;
7779
import static org.elasticsearch.xpack.esql.action.EsqlCapabilities.Cap.KNN_FUNCTION_V5;
@@ -97,6 +99,7 @@ public abstract class EsqlSpecTestCase extends ESRestTestCase {
9799
protected final String instructions;
98100
protected final Mode mode;
99101
protected static Boolean supportsTook;
102+
protected static Boolean supportsViews;
100103

101104
@ParametersFactory(argumentFormatting = "csv-spec:%2$s.%3$s")
102105
public static List<Object[]> readScriptSpec() throws Exception {
@@ -122,64 +125,74 @@ protected EsqlSpecTestCase(
122125
this.mode = randomFrom(Mode.values());
123126
}
124127

125-
private static final Object lock = new Object();
126-
private static volatile boolean dataLoaded = false;
127-
private static volatile boolean setupStarted = false;
128-
private static volatile Throwable setupFailure = null;
128+
private static class Protected {
129+
private final Object lock = new Object();
130+
private volatile boolean completed = false;
131+
private volatile boolean started = false;
132+
private volatile Throwable failure = null;
133+
134+
private void protectedBlock(Callable<Void> callable) {
135+
if (completed) {
136+
return;
137+
}
138+
// In case tests get run in parallel, we ensure only one setup is run, and other tests wait for this
139+
synchronized (lock) {
140+
if (completed) {
141+
return;
142+
}
143+
if (started) {
144+
// Should only happen if a previous test setup failed, possibly with partial setup, let's fail fast the current test
145+
if (failure != null) {
146+
fail(failure, "Previous test setup failed: " + failure.getMessage());
147+
}
148+
fail("Previous test setup failed with unknown error");
149+
}
150+
started = true;
151+
try {
152+
callable.call();
153+
completed = true;
154+
} catch (Throwable t) {
155+
failure = t;
156+
fail(failure, "Current test setup failed: " + failure.getMessage());
157+
}
158+
}
159+
}
160+
}
161+
162+
private static final Protected INGEST = new Protected();
163+
private static final Protected VIEWS = new Protected();
129164

130165
@Before
131166
public void setup() {
132-
protectedBlock(() -> {
133-
boolean supportsLookup = supportsIndexModeLookup();
134-
boolean supportsSourceMapping = supportsSourceFieldMapping();
135-
boolean supportsInferenceTestService = supportsInferenceTestService();
136-
if (supportsInferenceTestService) {
167+
INGEST.protectedBlock(() -> {
168+
// Inference endpoints must be created before ingesting any datasets that rely on them (mapping of inference_id)
169+
if (supportsInferenceTestService()) {
137170
createInferenceEndpoints(adminClient());
138171
}
139-
loadDataSetIntoEs(client(), supportsLookup, supportsSourceMapping, supportsInferenceTestService);
172+
loadDataSetIntoEs(client(), supportsIndexModeLookup(), supportsSourceFieldMapping(), supportsInferenceTestService());
140173
return null;
141174
});
142-
}
143-
144-
private static void protectedBlock(Callable<Void> callable) {
145-
if (dataLoaded) {
146-
return;
147-
}
148-
// In case tests get run in parallel, we ensure only one setup is run, and other tests wait for this
149-
synchronized (lock) {
150-
if (dataLoaded) {
151-
return;
175+
// Views can be created before or after ingest, since index resolution is currently only done on the combined query
176+
VIEWS.protectedBlock(() -> {
177+
if (supportsViews()) {
178+
loadViewsIntoEs(adminClient());
152179
}
153-
if (setupStarted) {
154-
// Should only happen if a previous test setup failed, possibly with partial setup, let's fail fast the current test
155-
if (setupFailure != null) {
156-
fail(setupFailure, "Previous test setup failed: " + setupFailure.getMessage());
157-
}
158-
fail("Previous test setup failed with unknown error");
159-
}
160-
setupStarted = true;
161-
try {
162-
callable.call();
163-
dataLoaded = true;
164-
} catch (Throwable t) {
165-
setupFailure = t;
166-
fail(setupFailure, "Current test setup failed: " + setupFailure.getMessage());
167-
}
168-
}
180+
return null;
181+
});
169182
}
170183

171184
@AfterClass
172185
public static void wipeTestData() throws IOException {
173186
try {
174-
dataLoaded = false;
187+
INGEST.completed = false;
175188
adminClient().performRequest(new Request("DELETE", "/*"));
176189
} catch (ResponseException e) {
177190
// 404 here just means we had no indexes
178191
if (e.getResponse().getStatusLine().getStatusCode() != 404) {
179192
throw e;
180193
}
181194
}
182-
195+
deleteViews(adminClient());
183196
deleteInferenceEndpoints(adminClient());
184197
}
185198

@@ -488,6 +501,13 @@ protected boolean supportsTook() throws IOException {
488501
return supportsTook;
489502
}
490503

504+
protected boolean supportsViews() {
505+
if (supportsViews == null) {
506+
supportsViews = hasCapabilities(adminClient(), List.of("views_v1"));
507+
}
508+
return supportsViews;
509+
}
510+
491511
private String tookKey(long took) {
492512
if (took < 10) {
493513
return "lt_10ms";

x-pack/plugin/esql/qa/testFixtures/src/main/java/org/elasticsearch/xpack/esql/CsvTestsDataLoader.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,9 +437,29 @@ private static void loadDataSetIntoEs(
437437
for (var policy : ENRICH_POLICIES) {
438438
loadEnrichPolicy(client, policy.policyName, policy.policyFileName, logger);
439439
}
440-
logger.info("Loading views");
441-
for (var view : VIEW_CONFIGS) {
442-
loadView(client, view.viewName, view.viewFileName, logger);
440+
}
441+
442+
public static void loadViewsIntoEs(RestClient client) throws IOException {
443+
Logger logger = LogManager.getLogger(CsvTestsDataLoader.class);
444+
if (clusterHasViewSupport(client, logger)) {
445+
logger.info("Loading views");
446+
for (var view : VIEW_CONFIGS) {
447+
loadView(client, view.viewName, view.viewFileName, logger);
448+
}
449+
} else {
450+
logger.info("Skipping loading views as the cluster does not support views");
451+
}
452+
}
453+
454+
public static void deleteViews(RestClient client) throws IOException {
455+
Logger logger = LogManager.getLogger(CsvTestsDataLoader.class);
456+
if (clusterHasViewSupport(client, logger)) {
457+
logger.info("Deleting views");
458+
for (var view : VIEW_CONFIGS) {
459+
deleteView(client, view.viewName, logger);
460+
}
461+
} else {
462+
logger.info("Skipping deleting views as the cluster does not support views");
443463
}
444464
}
445465

@@ -600,6 +620,31 @@ private static void loadView(RestClient client, String viewName, String viewFile
600620
logger.info("View creation response: {}", response.getStatusLine());
601621
}
602622

623+
private static boolean clusterHasViewSupport(RestClient client, Logger logger) throws IOException {
624+
Request request = new Request("GET", "/_query/views");
625+
try {
626+
Response response = client.performRequest(request);
627+
logger.info("View listing response: {}", response.getStatusLine());
628+
} catch (ResponseException e) {
629+
logger.info("View listing error: {}", e.getMessage());
630+
if (e.getResponse().getStatusLine().getStatusCode() == 400) {
631+
return false;
632+
}
633+
throw e;
634+
}
635+
return true;
636+
}
637+
638+
private static void deleteView(RestClient client, String viewName, Logger logger) throws IOException {
639+
try {
640+
client.performRequest(new Request("DELETE", "/_query/view/" + viewName));
641+
} catch (ResponseException e) {
642+
if (e.getResponse().getStatusLine().getStatusCode() != 404) {
643+
throw e;
644+
}
645+
}
646+
}
647+
603648
private static URL getResource(String name) {
604649
URL result = CsvTestsDataLoader.class.getResource(name);
605650
if (result == null) {

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,16 @@
8383
import org.elasticsearch.xpack.esql.querylog.EsqlQueryLog;
8484
import org.elasticsearch.xpack.esql.session.IndexResolver;
8585
import org.elasticsearch.xpack.esql.view.DeleteViewAction;
86+
import org.elasticsearch.xpack.esql.view.GetViewAction;
87+
import org.elasticsearch.xpack.esql.view.ListViewsAction;
8688
import org.elasticsearch.xpack.esql.view.PutViewAction;
8789
import org.elasticsearch.xpack.esql.view.RestDeleteViewAction;
90+
import org.elasticsearch.xpack.esql.view.RestGetViewAction;
91+
import org.elasticsearch.xpack.esql.view.RestListViewsAction;
8892
import org.elasticsearch.xpack.esql.view.RestPutViewAction;
8993
import org.elasticsearch.xpack.esql.view.TransportDeleteViewAction;
94+
import org.elasticsearch.xpack.esql.view.TransportGetViewAction;
95+
import org.elasticsearch.xpack.esql.view.TransportListViewsAction;
9096
import org.elasticsearch.xpack.esql.view.TransportPutViewAction;
9197
import org.elasticsearch.xpack.esql.view.ViewService;
9298

@@ -313,7 +319,9 @@ public List<ActionHandler> getActions() {
313319
new ActionHandler(EsqlListQueriesAction.INSTANCE, TransportEsqlListQueriesAction.class),
314320
new ActionHandler(EsqlGetQueryAction.INSTANCE, TransportEsqlGetQueryAction.class),
315321
new ActionHandler(PutViewAction.INSTANCE, TransportPutViewAction.class),
316-
new ActionHandler(DeleteViewAction.INSTANCE, TransportDeleteViewAction.class)
322+
new ActionHandler(DeleteViewAction.INSTANCE, TransportDeleteViewAction.class),
323+
new ActionHandler(GetViewAction.INSTANCE, TransportGetViewAction.class),
324+
new ActionHandler(ListViewsAction.INSTANCE, TransportListViewsAction.class)
317325
);
318326
}
319327

@@ -337,7 +345,9 @@ public List<RestHandler> getRestHandlers(
337345
new RestEsqlDeleteAsyncResultAction(),
338346
new RestEsqlListQueriesAction(),
339347
new RestPutViewAction(),
340-
new RestDeleteViewAction()
348+
new RestDeleteViewAction(),
349+
new RestGetViewAction(),
350+
new RestListViewsAction()
341351
);
342352
}
343353

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
package org.elasticsearch.xpack.esql.view;
8+
9+
import org.elasticsearch.action.ActionRequestValidationException;
10+
import org.elasticsearch.action.ActionType;
11+
import org.elasticsearch.action.support.master.AcknowledgedResponse;
12+
import org.elasticsearch.action.support.master.MasterNodeRequest;
13+
import org.elasticsearch.common.io.stream.StreamInput;
14+
import org.elasticsearch.common.io.stream.StreamOutput;
15+
import org.elasticsearch.core.TimeValue;
16+
17+
import java.io.IOException;
18+
import java.util.Objects;
19+
20+
public class GetViewAction extends ActionType<AcknowledgedResponse> {
21+
22+
public static final GetViewAction INSTANCE = new GetViewAction();
23+
public static final String NAME = "cluster:admin/xpack/esql/view/get";
24+
25+
private GetViewAction() {
26+
super(NAME);
27+
}
28+
29+
public static class Request extends MasterNodeRequest<GetViewAction.Request> {
30+
private final String name;
31+
32+
public Request(TimeValue masterNodeTimeout, String name) {
33+
super(masterNodeTimeout);
34+
this.name = Objects.requireNonNull(name, "name cannot be null");
35+
}
36+
37+
public Request(StreamInput in) throws IOException {
38+
super(in);
39+
name = in.readString();
40+
}
41+
42+
@Override
43+
public void writeTo(StreamOutput out) throws IOException {
44+
super.writeTo(out);
45+
out.writeString(name);
46+
}
47+
48+
public String name() {
49+
return name;
50+
}
51+
52+
@Override
53+
public ActionRequestValidationException validate() {
54+
return null;
55+
}
56+
57+
@Override
58+
public boolean equals(Object o) {
59+
if (this == o) return true;
60+
if (o == null || getClass() != o.getClass()) return false;
61+
Request request = (Request) o;
62+
return name.equals(request.name);
63+
}
64+
65+
@Override
66+
public int hashCode() {
67+
return name.hashCode();
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)