Skip to content

Commit aa8d0e2

Browse files
authored
Merge pull request #147 from bpintea/rest_endpoint
ESQL: add REST endpoint
2 parents f9276cf + e8b7386 commit aa8d0e2

File tree

15 files changed

+660
-2
lines changed

15 files changed

+660
-2
lines changed

x-pack/plugin/esql/qa/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
description = 'Integration tests for ESQL'
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
apply plugin: 'elasticsearch.java'
2+
3+
description = 'Integration tests for ESQL'
4+
5+
dependencies {
6+
api project(":test:framework")
7+
8+
// Common utilities from QL
9+
api project(xpackModule('ql:test-fixtures'))
10+
}
11+
12+
subprojects {
13+
if (subprojects.isEmpty()) {
14+
// leaf project
15+
} else {
16+
apply plugin: 'elasticsearch.java'
17+
apply plugin: 'elasticsearch.standalone-rest-test'
18+
}
19+
20+
21+
if (project.name != 'security') {
22+
// The security project just configures its subprojects
23+
apply plugin: 'elasticsearch.internal-java-rest-test'
24+
25+
testClusters.matching { it.name == "javaRestTest" }.configureEach {
26+
testDistribution = 'DEFAULT'
27+
setting 'xpack.ml.enabled', 'false'
28+
setting 'xpack.watcher.enabled', 'false'
29+
}
30+
31+
32+
dependencies {
33+
configurations.javaRestTestRuntimeClasspath {
34+
resolutionStrategy.force "org.slf4j:slf4j-api:1.7.25"
35+
}
36+
configurations.javaRestTestRuntimeOnly {
37+
// This is also required to make resolveAllDependencies work
38+
resolutionStrategy.force "org.slf4j:slf4j-api:1.7.25"
39+
}
40+
41+
/* Since we're a standalone rest test we actually get transitive
42+
* dependencies but we don't really want them because they cause
43+
* all kinds of trouble with the jar hell checks. So we suppress
44+
* them explicitly for non-es projects. */
45+
javaRestTestImplementation(project(':x-pack:plugin:esql:qa:server')) {
46+
transitive = false
47+
}
48+
javaRestTestImplementation project(":test:framework")
49+
javaRestTestRuntimeOnly project(xpackModule('ql:test-fixtures'))
50+
51+
javaRestTestRuntimeOnly "org.slf4j:slf4j-api:1.7.25"
52+
}
53+
}
54+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
testClusters.matching { it.name == "javaRestTest" }.configureEach {
2+
testDistribution = 'DEFAULT'
3+
setting 'xpack.security.enabled', 'false'
4+
setting 'xpack.license.self_generated.type', 'trial'
5+
plugin ':x-pack:qa:freeze-plugin'
6+
}
7+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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.qa.single_node;
8+
9+
import org.elasticsearch.xpack.esql.qa.rest.RestEsqlTestCase;
10+
11+
public class RestEsqlIT extends RestEsqlTestCase {}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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+
8+
package org.elasticsearch.xpack.esql.qa.rest;
9+
10+
import org.apache.http.HttpEntity;
11+
import org.apache.http.entity.ContentType;
12+
import org.apache.http.nio.entity.NByteArrayEntity;
13+
import org.elasticsearch.client.Request;
14+
import org.elasticsearch.client.RequestOptions;
15+
import org.elasticsearch.client.Response;
16+
import org.elasticsearch.common.xcontent.XContentHelper;
17+
import org.elasticsearch.test.rest.ESRestTestCase;
18+
import org.elasticsearch.xcontent.XContentBuilder;
19+
import org.elasticsearch.xcontent.XContentType;
20+
21+
import java.io.ByteArrayOutputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.io.OutputStream;
25+
import java.time.ZoneId;
26+
import java.util.Map;
27+
28+
import static java.util.Collections.emptySet;
29+
30+
public class RestEsqlTestCase extends ESRestTestCase {
31+
32+
public static class RequestObjectBuilder {
33+
private final XContentBuilder builder;
34+
private boolean isBuilt = false;
35+
36+
public RequestObjectBuilder() throws IOException {
37+
this(XContentType.JSON);
38+
}
39+
40+
public RequestObjectBuilder(XContentType type) throws IOException {
41+
builder = XContentBuilder.builder(type, emptySet(), emptySet());
42+
builder.startObject();
43+
}
44+
45+
public RequestObjectBuilder query(String query) throws IOException {
46+
builder.field("query", query);
47+
return this;
48+
}
49+
50+
public RequestObjectBuilder columnar(boolean columnar) throws IOException {
51+
builder.field("columnar", columnar);
52+
return this;
53+
}
54+
55+
public RequestObjectBuilder timeZone(ZoneId zoneId) throws IOException {
56+
builder.field("time_zone", zoneId);
57+
return this;
58+
}
59+
60+
public RequestObjectBuilder build() throws IOException {
61+
if (isBuilt == false) {
62+
builder.endObject();
63+
isBuilt = true;
64+
}
65+
return this;
66+
}
67+
68+
public OutputStream getOutputStream() throws IOException {
69+
if (isBuilt == false) {
70+
throw new IllegalStateException("object not yet built");
71+
}
72+
builder.flush();
73+
return builder.getOutputStream();
74+
}
75+
76+
public XContentType contentType() {
77+
return builder.contentType();
78+
}
79+
80+
public static RequestObjectBuilder jsonBuilder() throws IOException {
81+
return new RequestObjectBuilder(XContentType.JSON);
82+
}
83+
}
84+
85+
public void testGetAnswer() throws IOException {
86+
RequestObjectBuilder builder = new RequestObjectBuilder(randomFrom(XContentType.values()));
87+
Map<String, Object> answer = runEsql(builder.query(randomAlphaOfLength(10)).build());
88+
assertEquals(2, answer.size());
89+
assertTrue(answer.containsKey("columns"));
90+
assertTrue(answer.containsKey("values"));
91+
}
92+
93+
private static Map<String, Object> runEsql(RequestObjectBuilder requestObject) throws IOException {
94+
Request request = new Request("POST", "/_esql");
95+
request.addParameter("error_trace", "true");
96+
String mediaType = requestObject.contentType().mediaTypeWithoutParameters();
97+
98+
try (ByteArrayOutputStream bos = (ByteArrayOutputStream) requestObject.getOutputStream()) {
99+
request.setEntity(new NByteArrayEntity(bos.toByteArray(), ContentType.getByMimeType(mediaType)));
100+
}
101+
102+
RequestOptions.Builder options = request.getOptions().toBuilder();
103+
options.addHeader("Accept", mediaType);
104+
options.addHeader("Content-Type", mediaType);
105+
request.setOptions(options);
106+
107+
Response response = client().performRequest(request);
108+
HttpEntity entity = response.getEntity();
109+
try (InputStream content = entity.getContent()) {
110+
XContentType xContentType = XContentType.fromMediaType(entity.getContentType().getValue());
111+
assertNotNull(xContentType);
112+
return XContentHelper.convertToMap(xContentType.xContent(), content, false);
113+
}
114+
}
115+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
8+
package org.elasticsearch.xpack.esql.action;
9+
10+
import org.elasticsearch.plugins.Plugin;
11+
import org.elasticsearch.test.ESIntegTestCase;
12+
import org.elasticsearch.xpack.esql.plugin.EsqlPlugin;
13+
14+
import java.util.Collection;
15+
import java.util.Collections;
16+
17+
import static org.elasticsearch.test.ESIntegTestCase.Scope.SUITE;
18+
19+
@ESIntegTestCase.ClusterScope(scope = SUITE, numDataNodes = 0, numClientNodes = 0, maxNumDataNodes = 0)
20+
public class EsqlActionIT extends ESIntegTestCase {
21+
22+
public void testEsqlAction() {
23+
EsqlQueryResponse response = new EsqlQueryRequestBuilder(client(), EsqlQueryAction.INSTANCE).query(randomAlphaOfLength(10)).get();
24+
assertNotNull(response);
25+
}
26+
27+
@Override
28+
protected Collection<Class<? extends Plugin>> nodePlugins() {
29+
return Collections.singletonList(EsqlPlugin.class);
30+
}
31+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
8+
package org.elasticsearch.xpack.esql.action;
9+
10+
import org.elasticsearch.action.ActionType;
11+
12+
public class EsqlQueryAction extends ActionType<EsqlQueryResponse> {
13+
14+
public static final EsqlQueryAction INSTANCE = new EsqlQueryAction();
15+
public static final String NAME = "indices:data/read/esql";
16+
17+
private EsqlQueryAction() {
18+
super(NAME, EsqlQueryResponse::new);
19+
}
20+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
8+
package org.elasticsearch.xpack.esql.action;
9+
10+
import org.elasticsearch.action.ActionRequest;
11+
import org.elasticsearch.action.ActionRequestValidationException;
12+
import org.elasticsearch.action.CompositeIndicesRequest;
13+
import org.elasticsearch.common.Strings;
14+
import org.elasticsearch.common.io.stream.StreamInput;
15+
import org.elasticsearch.xcontent.ObjectParser;
16+
import org.elasticsearch.xcontent.ParseField;
17+
import org.elasticsearch.xcontent.XContentParser;
18+
19+
import java.io.IOException;
20+
import java.time.ZoneId;
21+
import java.util.function.Supplier;
22+
23+
import static org.elasticsearch.action.ValidateActions.addValidationError;
24+
25+
public class EsqlQueryRequest extends ActionRequest implements CompositeIndicesRequest {
26+
27+
private static final ParseField QUERY_FIELD = new ParseField("query");
28+
private static final ParseField COLUMNAR_FIELD = new ParseField("columnar"); // TODO -> "mode"?
29+
private static final ParseField TIME_ZONE_FIELD = new ParseField("time_zone");
30+
31+
private static final ObjectParser<EsqlQueryRequest, Void> PARSER = objectParser(EsqlQueryRequest::new);
32+
33+
private String query;
34+
private boolean columnar;
35+
private ZoneId zoneId;
36+
37+
public EsqlQueryRequest(StreamInput in) throws IOException {
38+
super(in);
39+
}
40+
41+
@Override
42+
public ActionRequestValidationException validate() {
43+
ActionRequestValidationException validationException = null;
44+
if (Strings.hasText(query) == false) {
45+
validationException = addValidationError("[query] is required", null);
46+
}
47+
return validationException;
48+
}
49+
50+
public EsqlQueryRequest() {}
51+
52+
public void query(String query) {
53+
this.query = query;
54+
}
55+
56+
public String query() {
57+
return query;
58+
}
59+
60+
public void columnar(boolean columnar) {
61+
this.columnar = columnar;
62+
}
63+
64+
public boolean columnar() {
65+
return columnar;
66+
}
67+
68+
public void zoneId(ZoneId zoneId) {
69+
this.zoneId = zoneId;
70+
}
71+
72+
public ZoneId zoneId() {
73+
return zoneId;
74+
}
75+
76+
public static EsqlQueryRequest fromXContent(XContentParser parser) {
77+
return PARSER.apply(parser, null);
78+
}
79+
80+
private static ObjectParser<EsqlQueryRequest, Void> objectParser(Supplier<EsqlQueryRequest> supplier) {
81+
ObjectParser<EsqlQueryRequest, Void> parser = new ObjectParser<>("esql/query", false, supplier);
82+
parser.declareString(EsqlQueryRequest::query, QUERY_FIELD);
83+
parser.declareBoolean(EsqlQueryRequest::columnar, COLUMNAR_FIELD);
84+
parser.declareString((request, zoneId) -> request.zoneId(ZoneId.of(zoneId)), TIME_ZONE_FIELD);
85+
return parser;
86+
}
87+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
8+
package org.elasticsearch.xpack.esql.action;
9+
10+
import org.elasticsearch.action.ActionRequestBuilder;
11+
import org.elasticsearch.client.internal.ElasticsearchClient;
12+
13+
import java.time.ZoneId;
14+
15+
public class EsqlQueryRequestBuilder extends ActionRequestBuilder<EsqlQueryRequest, EsqlQueryResponse> {
16+
17+
public EsqlQueryRequestBuilder(ElasticsearchClient client, EsqlQueryAction action, EsqlQueryRequest request) {
18+
super(client, action, request);
19+
}
20+
21+
public EsqlQueryRequestBuilder(ElasticsearchClient client, EsqlQueryAction action) {
22+
this(client, action, new EsqlQueryRequest());
23+
}
24+
25+
public EsqlQueryRequestBuilder query(String query) {
26+
request.query(query);
27+
return this;
28+
}
29+
30+
public EsqlQueryRequestBuilder columnar(boolean columnar) {
31+
request.columnar(columnar);
32+
return this;
33+
}
34+
35+
public EsqlQueryRequestBuilder timeZone(ZoneId zoneId) {
36+
request.zoneId(zoneId);
37+
return this;
38+
}
39+
}

0 commit comments

Comments
 (0)