From 5ec298acc9b0dc700932ec3de0bb8292f247e01e Mon Sep 17 00:00:00 2001 From: Youssef Aouichaoui Date: Wed, 31 Jul 2024 19:04:01 +0200 Subject: [PATCH 1/6] Support for SQL. Signed-off-by: Youssef Aouichaoui --- .../client/elc/ElasticsearchTemplate.java | 34 ++ .../client/elc/ResponseConverter.java | 76 ++- .../core/ElasticsearchOperations.java | 3 +- .../elasticsearch/core/query/SqlQuery.java | 498 ++++++++++++++++++ .../elasticsearch/core/sql/SqlOperations.java | 28 + .../elasticsearch/core/sql/SqlResponse.java | 224 ++++++++ .../elasticsearch/core/sql/package-info.java | 6 + .../core/sql/types/ResponseFormat.java | 29 + .../core/sql/types/package-info.java | 4 + .../sql/SqlOperationsIntegrationTests.java | 147 ++++++ 10 files changed, 1029 insertions(+), 20 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/sql/package-info.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/sql/types/ResponseFormat.java create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/sql/types/package-info.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java index 444d744ab..0f4a4147c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java @@ -16,6 +16,7 @@ package org.springframework.data.elasticsearch.client.elc; import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*; +import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.json; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.Time; @@ -23,6 +24,8 @@ import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; import co.elastic.clients.elasticsearch.core.msearch.MultiSearchResponseItem; import co.elastic.clients.elasticsearch.core.search.ResponseBody; +import co.elastic.clients.elasticsearch.sql.ElasticsearchSqlClient; +import co.elastic.clients.elasticsearch.sql.QueryResponse; import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.transport.Version; @@ -34,6 +37,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -56,6 +60,7 @@ import org.springframework.data.elasticsearch.core.reindex.ReindexRequest; import org.springframework.data.elasticsearch.core.reindex.ReindexResponse; import org.springframework.data.elasticsearch.core.script.Script; +import org.springframework.data.elasticsearch.core.sql.SqlResponse; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -74,6 +79,7 @@ public class ElasticsearchTemplate extends AbstractElasticsearchTemplate { private static final Log LOGGER = LogFactory.getLog(ElasticsearchTemplate.class); private final ElasticsearchClient client; + private final ElasticsearchSqlClient sqlClient; private final RequestConverter requestConverter; private final ResponseConverter responseConverter; private final JsonpMapper jsonpMapper; @@ -85,6 +91,7 @@ public ElasticsearchTemplate(ElasticsearchClient client) { Assert.notNull(client, "client must not be null"); this.client = client; + this.sqlClient = client.sql(); this.jsonpMapper = client._transport().jsonpMapper(); requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper); responseConverter = new ResponseConverter(jsonpMapper); @@ -97,6 +104,7 @@ public ElasticsearchTemplate(ElasticsearchClient client, ElasticsearchConverter Assert.notNull(client, "client must not be null"); this.client = client; + this.sqlClient = client.sql(); this.jsonpMapper = client._transport().jsonpMapper(); requestConverter = new RequestConverter(elasticsearchConverter, jsonpMapper); responseConverter = new ResponseConverter(jsonpMapper); @@ -656,6 +664,32 @@ public boolean deleteScript(String name) { DeleteScriptRequest request = requestConverter.scriptDelete(name); return execute(client -> client.deleteScript(request)).acknowledged(); } + + @Override + public SqlResponse search(SqlQuery query) { + Assert.notNull(query, "Query must not be null."); + Assert.isTrue(query.getFormat() == null || json.equals(query.getFormat()), + "The Elasticsearch Java Client only supports JSON format."); + + try { + QueryResponse response = sqlClient.query(sqb -> { + sqb.query(query.getQuery()).catalog(query.getCatalog()).columnar(query.getColumnar()).cursor(query.getCursor()) + .fetchSize(query.getFetchSize()).fieldMultiValueLeniency(query.getFieldMultiValueLeniency()) + .indexUsingFrozen(query.getIndexIncludeFrozen()).keepAlive(time(query.getKeepAlive())) + .keepOnCompletion(query.getKeepOnCompletion()).pageTimeout(time(query.getPageTimeout())) + .requestTimeout(time(query.getRequestTimeout())) + .waitForCompletionTimeout(time(query.getWaitForCompletionTimeout())) + .filter(requestConverter.getQuery(query.getFilter(), null)) + .timeZone(Objects.toString(query.getTimeZone(), null)).format(Objects.toString(query.getFormat(), null)); + + return sqb; + }); + + return responseConverter.sqlResponse(response); + } catch (IOException e) { + throw exceptionTranslator.translateException(e); + } + } // endregion // region client callback diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ResponseConverter.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ResponseConverter.java index 1dada5d11..232de0151 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ResponseConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ResponseConverter.java @@ -15,25 +15,9 @@ */ package org.springframework.data.elasticsearch.client.elc; -import static org.springframework.data.elasticsearch.client.elc.JsonUtils.*; -import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*; - -import co.elastic.clients.elasticsearch._types.BulkIndexByScrollFailure; -import co.elastic.clients.elasticsearch._types.ErrorCause; -import co.elastic.clients.elasticsearch._types.Time; -import co.elastic.clients.elasticsearch._types.query_dsl.Query; -import co.elastic.clients.elasticsearch.cluster.ComponentTemplateSummary; -import co.elastic.clients.elasticsearch.cluster.GetComponentTemplateResponse; -import co.elastic.clients.elasticsearch.cluster.HealthResponse; -import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse; -import co.elastic.clients.elasticsearch.core.GetScriptResponse; -import co.elastic.clients.elasticsearch.core.UpdateByQueryResponse; -import co.elastic.clients.elasticsearch.core.mget.MultiGetError; -import co.elastic.clients.elasticsearch.core.mget.MultiGetResponseItem; -import co.elastic.clients.elasticsearch.indices.*; -import co.elastic.clients.elasticsearch.indices.get_index_template.IndexTemplateItem; -import co.elastic.clients.elasticsearch.indices.get_mapping.IndexMappingRecord; -import co.elastic.clients.json.JsonpMapper; +import static org.springframework.data.elasticsearch.client.elc.JsonUtils.toJson; +import static org.springframework.data.elasticsearch.client.elc.TypeUtils.removePrefixFromJson; +import static org.springframework.data.elasticsearch.client.elc.TypeUtils.typeMapping; import java.util.ArrayList; import java.util.HashMap; @@ -61,10 +45,41 @@ import org.springframework.data.elasticsearch.core.query.StringQuery; import org.springframework.data.elasticsearch.core.reindex.ReindexResponse; import org.springframework.data.elasticsearch.core.script.Script; +import org.springframework.data.elasticsearch.core.sql.SqlResponse; import org.springframework.data.elasticsearch.support.DefaultStringObjectMap; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import co.elastic.clients.elasticsearch._types.BulkIndexByScrollFailure; +import co.elastic.clients.elasticsearch._types.ErrorCause; +import co.elastic.clients.elasticsearch._types.Time; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch.cluster.ComponentTemplateSummary; +import co.elastic.clients.elasticsearch.cluster.GetComponentTemplateResponse; +import co.elastic.clients.elasticsearch.cluster.HealthResponse; +import co.elastic.clients.elasticsearch.core.DeleteByQueryResponse; +import co.elastic.clients.elasticsearch.core.GetScriptResponse; +import co.elastic.clients.elasticsearch.core.UpdateByQueryResponse; +import co.elastic.clients.elasticsearch.core.mget.MultiGetError; +import co.elastic.clients.elasticsearch.core.mget.MultiGetResponseItem; +import co.elastic.clients.elasticsearch.indices.Alias; +import co.elastic.clients.elasticsearch.indices.AliasDefinition; +import co.elastic.clients.elasticsearch.indices.GetAliasResponse; +import co.elastic.clients.elasticsearch.indices.GetIndexResponse; +import co.elastic.clients.elasticsearch.indices.GetIndexTemplateResponse; +import co.elastic.clients.elasticsearch.indices.GetIndicesSettingsResponse; +import co.elastic.clients.elasticsearch.indices.GetMappingResponse; +import co.elastic.clients.elasticsearch.indices.GetTemplateResponse; +import co.elastic.clients.elasticsearch.indices.IndexSettings; +import co.elastic.clients.elasticsearch.indices.IndexState; +import co.elastic.clients.elasticsearch.indices.IndexTemplateSummary; +import co.elastic.clients.elasticsearch.indices.TemplateMapping; +import co.elastic.clients.elasticsearch.indices.get_index_template.IndexTemplateItem; +import co.elastic.clients.elasticsearch.indices.get_mapping.IndexMappingRecord; +import co.elastic.clients.elasticsearch.sql.QueryResponse; +import co.elastic.clients.json.JsonData; +import co.elastic.clients.json.JsonpMapper; + /** * Class to convert Elasticsearch responses into Spring Data Elasticsearch classes. * @@ -536,6 +551,29 @@ public Script scriptResponse(GetScriptResponse response) { } // endregion + // region sql + public SqlResponse sqlResponse(QueryResponse response) { + SqlResponse.Builder builder = SqlResponse.builder(); + builder.withRunning(Boolean.TRUE.equals(response.isRunning())) + .withPartial(Boolean.TRUE.equals(response.isPartial())).withCursor(response.cursor()); + + final List columns = response.columns().stream() + .map(column -> new SqlResponse.Column(column.name(), column.type())).toList(); + builder.withColumns(columns); + + for (List rowValues : response.rows()) { + SqlResponse.Row.Builder rowBuilder = SqlResponse.Row.builder(); + for (int idx = 0; idx < rowValues.size(); idx++) { + rowBuilder.withValue(columns.get(idx), rowValues.get(idx).toJson()); + } + + builder.withRow(rowBuilder.build()); + } + + return builder.build(); + } + // end region + // region helper functions private long timeToLong(Time time) { diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java index 324886892..b1427bcc1 100755 --- a/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ElasticsearchOperations.java @@ -20,6 +20,7 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.routing.RoutingResolver; import org.springframework.data.elasticsearch.core.script.ScriptOperations; +import org.springframework.data.elasticsearch.core.sql.SqlOperations; import org.springframework.lang.Nullable; /** @@ -35,7 +36,7 @@ * @author Dmitriy Yakovlev * @author Peter-Josef Meisch */ -public interface ElasticsearchOperations extends DocumentOperations, SearchOperations, ScriptOperations { +public interface ElasticsearchOperations extends DocumentOperations, SearchOperations, ScriptOperations, SqlOperations { /** * get an {@link IndexOperations} that is bound to the given class diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java new file mode 100644 index 000000000..452ce9270 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java @@ -0,0 +1,498 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.query; + +import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.cbor; +import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.csv; +import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.json; +import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.smile; +import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.yaml; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.TimeZone; + +import org.springframework.data.elasticsearch.core.sql.types.ResponseFormat; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Defines an SQL request. + * + * @author Aouichaoui Youssef + * @see docs + * @since 5.4 + */ +public class SqlQuery { + /** + * Separator for CSV results. + *

+ * Default, this is set to {@code ,}. + */ + @Nullable private final String delimiter; + + /** + * The format for the response, such as csv, json, txt, can be viewed at the {@link ResponseFormat}. The java client + * of Elasticsearch only supports JSON. See {@link co.elastic.clients.transport.JsonEndpoint} + *

+ * Default, this is set to {@code json}. + */ + @Nullable private final ResponseFormat format; + + /** + * If true, returns partial results if there are shard request timeouts or shard failures. + *

+ * Default, this is set to {@code false}. + */ + @Nullable private final Boolean allowPartialSearchResults; + + /** + * Default catalog/cluster for queries. If unspecified, the queries are executed on the data in the local cluster + * only. + */ + @Nullable private final String catalog; + + /** + * If true, returns results in a columnar format. + *

+ * Default, this is set to {@code false}. + */ + @Nullable private final Boolean columnar; + + /** + * To retrieve a set of paginated results, ignore other request body parameters when specifying a cursor and using the + * {@link #columnar} and {@link #timeZone} parameters. + */ + @Nullable private final String cursor; + + /** + * Maximum number of rows to return in the response. + *

+ * Default, this is set to {@code 1000}. + */ + @Nullable private final Integer fetchSize; + + /** + * If false, the API returns an error for fields containing array values. + *

+ * Default, this is set to {@code false}. + */ + @Nullable private final Boolean fieldMultiValueLeniency; + + /** + * Query that filter documents for the SQL search. + */ + @Nullable private final Query filter; + + /** + * If true, the search can run on frozen indices. + *

+ * Default, this is set to {@code false}. + */ + @Nullable private final Boolean indexIncludeFrozen; + + /** + * Retention period for an async or saved synchronous search. + *

+ * Default, this is set to {@code 5 days}. + */ + @Nullable private final Duration keepAlive; + + /** + * If it is true, it will store synchronous searches when the {@link #waitForCompletionTimeout} parameter is + * specified. + */ + @Nullable private final Boolean keepOnCompletion; + + /** + * Minimum retention period for the scroll cursor. + *

+ * Default, this is set to {@code 45 seconds}. + */ + @Nullable private final Duration pageTimeout; + + /** + * Timeout before the request fails. + *

+ * Default, this is set to {@code 90 seconds}. + */ + @Nullable private final Duration requestTimeout; + + /** + * Values for parameters in the query. + */ + @Nullable private final List params; + + /** + * SQL query to run. + */ + private final String query; + + /** + * Time zone ID for the search. + *

+ * Default, this is set to {@code UTC}. + */ + @Nullable private final TimeZone timeZone; + + /** + * Period to wait for complete results. + *

+ * Default, this is set to no timeout. + */ + @Nullable private final Duration waitForCompletionTimeout; + + private SqlQuery(Builder builder) { + this.delimiter = builder.delimiter; + this.format = builder.format; + + this.allowPartialSearchResults = builder.allowPartialSearchResults; + + this.catalog = builder.catalog; + this.columnar = builder.columnar; + this.cursor = builder.cursor; + + this.fetchSize = builder.fetchSize; + this.fieldMultiValueLeniency = builder.fieldMultiValueLeniency; + + this.filter = builder.filter; + + this.indexIncludeFrozen = builder.indexIncludeFrozen; + this.keepAlive = builder.keepAlive; + this.keepOnCompletion = builder.keepOnCompletion; + + this.pageTimeout = builder.pageTimeout; + this.requestTimeout = builder.requestTimeout; + + this.params = builder.params; + this.query = builder.query; + + this.timeZone = builder.timeZone; + this.waitForCompletionTimeout = builder.waitForCompletionTimeout; + } + + @Nullable + public String getDelimiter() { + return delimiter; + } + + @Nullable + public ResponseFormat getFormat() { + return format; + } + + @Nullable + public Boolean getAllowPartialSearchResults() { + return allowPartialSearchResults; + } + + @Nullable + public String getCatalog() { + return catalog; + } + + @Nullable + public Boolean getColumnar() { + return columnar; + } + + @Nullable + public String getCursor() { + return cursor; + } + + @Nullable + public Integer getFetchSize() { + return fetchSize; + } + + @Nullable + public Boolean getFieldMultiValueLeniency() { + return fieldMultiValueLeniency; + } + + @Nullable + public Query getFilter() { + return filter; + } + + @Nullable + public Boolean getIndexIncludeFrozen() { + return indexIncludeFrozen; + } + + @Nullable + public Duration getKeepAlive() { + return keepAlive; + } + + @Nullable + public Boolean getKeepOnCompletion() { + return keepOnCompletion; + } + + @Nullable + public Duration getPageTimeout() { + return pageTimeout; + } + + @Nullable + public Duration getRequestTimeout() { + return requestTimeout; + } + + @Nullable + public List getParams() { + return params; + } + + public String getQuery() { + return query; + } + + @Nullable + public TimeZone getTimeZone() { + return timeZone; + } + + @Nullable + public Duration getWaitForCompletionTimeout() { + return waitForCompletionTimeout; + } + + public static Builder builder(String query) { + return new Builder(query); + } + + public static class Builder { + @Nullable private String delimiter; + @Nullable private ResponseFormat format; + + @Nullable private Boolean allowPartialSearchResults; + + @Nullable private String catalog; + @Nullable private Boolean columnar; + @Nullable private String cursor; + + @Nullable private Integer fetchSize; + @Nullable private Boolean fieldMultiValueLeniency; + + @Nullable private Query filter; + + @Nullable private Boolean indexIncludeFrozen; + + @Nullable private Duration keepAlive; + @Nullable private Boolean keepOnCompletion; + + @Nullable private Duration pageTimeout; + @Nullable private Duration requestTimeout; + + @Nullable private List params; + private final String query; + + @Nullable private TimeZone timeZone; + @Nullable private Duration waitForCompletionTimeout; + + private Builder(String query) { + Assert.notNull(query, "query must not be null"); + + this.query = query; + } + + /** + * Separator for CSV results. + */ + public Builder withDelimiter(String delimiter) { + this.delimiter = delimiter; + + return this; + } + + /** + * The format for the response, such as csv, json, txt, can be viewed at the {@link ResponseFormat}. + */ + public Builder withFormat(ResponseFormat format) { + this.format = format; + + return this; + } + + /** + * If true, returns partial results if there are shard request timeouts or shard failures. + */ + public Builder withAllowPartialSearchResults(Boolean allowPartialSearchResults) { + this.allowPartialSearchResults = allowPartialSearchResults; + + return this; + } + + /** + * Default catalog/cluster for queries. If unspecified, the queries are executed on the data in the local cluster + * only. + */ + public Builder withCatalog(String catalog) { + this.catalog = catalog; + + return this; + } + + /** + * If true, returns results in a columnar format. + */ + public Builder withColumnar(Boolean columnar) { + this.columnar = columnar; + + return this; + } + + /** + * To retrieve a set of paginated results, ignore other request body parameters when specifying a cursor and using + * the {@link #columnar} and {@link #timeZone} parameters. + */ + public Builder withCursor(String cursor) { + this.cursor = cursor; + + return this; + } + + /** + * Maximum number of rows to return in the response. + */ + public Builder withFetchSize(Integer fetchSize) { + this.fetchSize = fetchSize; + + return this; + } + + /** + * If false, the API returns an error for fields containing array values. + */ + public Builder withFieldMultiValueLeniency(Boolean fieldMultiValueLeniency) { + this.fieldMultiValueLeniency = fieldMultiValueLeniency; + + return this; + } + + /** + * Query that filter documents for the SQL search. + */ + public Builder setFilter(Query filter) { + this.filter = filter; + + return this; + } + + /** + * If true, the search can run on frozen indices. + */ + public Builder withIndexIncludeFrozen(Boolean indexIncludeFrozen) { + this.indexIncludeFrozen = indexIncludeFrozen; + + return this; + } + + /** + * Retention period for an async or saved synchronous search. + */ + public Builder setKeepAlive(Duration keepAlive) { + this.keepAlive = keepAlive; + + return this; + } + + /** + * If it is true, it will store synchronous searches when the {@link #waitForCompletionTimeout} parameter is + * specified. + */ + public Builder withKeepOnCompletion(Boolean keepOnCompletion) { + this.keepOnCompletion = keepOnCompletion; + + return this; + } + + /** + * Minimum retention period for the scroll cursor. + */ + public Builder withPageTimeout(Duration pageTimeout) { + this.pageTimeout = pageTimeout; + + return this; + } + + /** + * Timeout before the request fails. + */ + public Builder withRequestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + + return this; + } + + /** + * Values for parameters in the query. + */ + public Builder withParams(List params) { + this.params = params; + + return this; + } + + /** + * Value for parameters in the query. + */ + public Builder withParam(Object param) { + if (this.params == null) { + this.params = new ArrayList<>(); + } + this.params.add(param); + + return this; + } + + /** + * Time zone ID for the search. + */ + public Builder withTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + + return this; + } + + /** + * Period to wait for complete results. + */ + public Builder withWaitForCompletionTimeout(Duration waitForCompletionTimeout) { + this.waitForCompletionTimeout = waitForCompletionTimeout; + + return this; + } + + public SqlQuery build() { + if (Boolean.TRUE.equals(columnar)) { + if (!(cbor.equals(format) || json.equals(format) || smile.equals(format) || yaml.equals(format))) { + throw new IllegalArgumentException("Columnar format support only YAML, CBOR, JSON and SMILE."); + } + } + + if (delimiter != null && !csv.equals(format)) { + throw new IllegalArgumentException("Delimiter support is only available for CSV responses."); + } + + return new SqlQuery(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java new file mode 100644 index 000000000..c07d7b91c --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.sql; + +import org.springframework.data.elasticsearch.core.query.SqlQuery; + +public interface SqlOperations { + /** + * Execute the criteria {@code query} against elasticsearch and return result as {@link SqlResponse} + * + * @param query the query to execute + * @return {@link SqlResponse} containing the list of found objects + */ + SqlResponse search(SqlQuery query); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java new file mode 100644 index 000000000..20c1651ae --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java @@ -0,0 +1,224 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.sql; + +import static java.util.Collections.unmodifiableList; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +import jakarta.json.JsonValue; + +/** + * Defines an SQL response. + * + * @author Aouichaoui Youssef + * @see docs + * @since 5.4 + */ +public class SqlResponse { + /** + * If {@code true}, the search is still running. + */ + private final boolean running; + + /** + * If {@code true}, the response does not contain complete search results. + */ + private final boolean partial; + + /** + * Cursor for the next set of paginated results. + */ + @Nullable private final String cursor; + + /** + * Column headings for the search results. + */ + private final List columns; + + /** + * Values for the search results. + */ + private final List rows; + + private SqlResponse(Builder builder) { + this.running = builder.running; + this.partial = builder.partial; + + this.cursor = builder.cursor; + + this.columns = unmodifiableList(builder.columns); + this.rows = unmodifiableList(builder.rows); + } + + public boolean isRunning() { + return running; + } + + public boolean isPartial() { + return partial; + } + + @Nullable + public String getCursor() { + return cursor; + } + + public List getColumns() { + return columns; + } + + public List getRows() { + return rows; + } + + public static Builder builder() { + return new Builder(); + } + + public record Column(String name, String type) { + } + + public static class Row implements Iterable> { + private final Map row; + + private Row(Builder builder) { + this.row = builder.row; + } + + public static Builder builder() { + return new Builder(); + } + + @NonNull + @Override + public Iterator> iterator() { + return row.entrySet().iterator(); + } + + /** + * This method should attempt to convert the SQL response into a document if the columns selected in an SQL query + * correspond to the fields in the document. + * + * @param documentClass The class that represents the document. + * @return an instance of Document {@link T}. + * @param Document type. + */ + public T toDocument(Class documentClass) { + throw new UnsupportedOperationException("Not implemented yet."); + } + + public static class Builder { + private final Map row = new HashMap<>(); + + public Builder withValue(Column column, JsonValue value) { + this.row.put(column, value); + + return this; + } + + public Row build() { + return new Row(this); + } + } + } + + public static class Builder { + private boolean running; + private boolean partial; + + @Nullable private String cursor; + + private final List columns = new ArrayList<>(); + private final List rows = new ArrayList<>(); + + private Builder() {} + + /** + * If {@code true}, the search is still running. + */ + public Builder withRunning(boolean running) { + this.running = running; + + return this; + } + + /** + * If {@code true}, the response does not contain complete search results. + */ + public Builder withPartial(boolean partial) { + this.partial = partial; + + return this; + } + + /** + * Cursor for the next set of paginated results. + */ + public Builder withCursor(@Nullable String cursor) { + this.cursor = cursor; + + return this; + } + + /** + * Column headings for the search results. + */ + public Builder withColumns(List columns) { + this.columns.addAll(columns); + + return this; + } + + /** + * Column heading for the search results. + */ + public Builder withColumn(Column column) { + this.columns.add(column); + + return this; + } + + /** + * Values for the search results. + */ + public Builder withRows(List rows) { + this.rows.addAll(rows); + + return this; + } + + /** + * Value for the search results. + */ + public Builder withRow(Row row) { + this.rows.add(row); + + return this; + } + + public SqlResponse build() { + return new SqlResponse(this); + } + } +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/package-info.java new file mode 100644 index 000000000..306bceade --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/package-info.java @@ -0,0 +1,6 @@ +/** + * Classes and interfaces to access to SQL API of Elasticsearch. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.elasticsearch.core.sql; diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/types/ResponseFormat.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/types/ResponseFormat.java new file mode 100644 index 000000000..4b9d0e8f5 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/types/ResponseFormat.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.sql.types; + +public enum ResponseFormat { + // text + csv, + json, + tsv, + txt, + yaml, + + // binary + cbor, + smile +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/types/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/types/package-info.java new file mode 100644 index 000000000..335c5fe3c --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/types/package-info.java @@ -0,0 +1,4 @@ +/** + * SQL types and enums used across spring-data-elasticsearch. + */ +package org.springframework.data.elasticsearch.core.sql.types; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java new file mode 100644 index 000000000..8317f62e4 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.sql; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.yaml; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.IndexOperations; +import org.springframework.data.elasticsearch.core.query.SqlQuery; +import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; + +/** + * Testing the querying using SQL syntax. + * + * @author Youssef Aouichaoui + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { SqlOperationsIntegrationTests.Config.class }) +@DisplayName("Using Elasticsearch SQL Client") +class SqlOperationsIntegrationTests { + @Autowired ElasticsearchOperations operations; + @Nullable IndexOperations indexOps; + + @BeforeEach + void setUp() { + // create index + indexOps = operations.indexOps(EntityForSQL.class); + indexOps.createWithMapping(); + + // add data + operations.save(EntityForSQL.builder().withViews(3).build(), EntityForSQL.builder().withViews(0).build()); + } + + @AfterEach + void tearDown() { + // delete index + if (indexOps != null) { + indexOps.delete(); + } + } + + // begin configuration region + @Configuration + @Import({ ElasticsearchTemplateConfiguration.class }) + static class Config {} + // end region + + @Test + void when_search_with_an_sql_query() { + // Given + SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").build(); + + // When + + // Then + SqlResponse response = operations.search(query); + assertNotNull(response); + assertFalse(response.getRows().isEmpty()); + assertEquals(1, response.getRows().size()); + } + + @Test + void when_search_with_an_sql_query_and_txt_format() { + // Given + SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").withFormat(yaml).build(); + + // When + + // Then + assertThrows(IllegalArgumentException.class, () -> operations.search(query), + "The Elasticsearch Java Client only supports JSON format."); + } + + // begin region + @Document(indexName = "entity_for_sql") + static class EntityForSQL { + @Nullable + @Id private String id; + private final Integer views; + + public EntityForSQL(Builder builder) { + this.views = builder.views; + } + + @Nullable + public String getId() { + return id; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + public Integer getViews() { + return views; + } + + public static Builder builder() { + return new Builder(); + } + + static class Builder { + private Integer views; + + public Builder withViews(Integer views) { + this.views = views; + + return this; + } + + public EntityForSQL build() { + return new EntityForSQL(this); + } + } + } + // end region + +} From 1c75282480208ec037a59f475c40a8518514a245 Mon Sep 17 00:00:00 2001 From: Youssef Aouichaoui Date: Thu, 1 Aug 2024 13:49:47 +0200 Subject: [PATCH 2/6] Map the request to the correct class for mapping elasticsearch queries. Signed-off-by: Youssef Aouichaoui --- .../client/elc/ElasticsearchTemplate.java | 14 +------------- .../client/elc/RequestConverter.java | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java index 0f4a4147c..8ef4413b6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java @@ -37,7 +37,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.stream.Collectors; import org.apache.commons.logging.Log; @@ -672,18 +671,7 @@ public SqlResponse search(SqlQuery query) { "The Elasticsearch Java Client only supports JSON format."); try { - QueryResponse response = sqlClient.query(sqb -> { - sqb.query(query.getQuery()).catalog(query.getCatalog()).columnar(query.getColumnar()).cursor(query.getCursor()) - .fetchSize(query.getFetchSize()).fieldMultiValueLeniency(query.getFieldMultiValueLeniency()) - .indexUsingFrozen(query.getIndexIncludeFrozen()).keepAlive(time(query.getKeepAlive())) - .keepOnCompletion(query.getKeepOnCompletion()).pageTimeout(time(query.getPageTimeout())) - .requestTimeout(time(query.getRequestTimeout())) - .waitForCompletionTimeout(time(query.getWaitForCompletionTimeout())) - .filter(requestConverter.getQuery(query.getFilter(), null)) - .timeZone(Objects.toString(query.getTimeZone(), null)).format(Objects.toString(query.getFormat(), null)); - - return sqb; - }); + QueryResponse response = sqlClient.query(requestConverter.sqlQueryRequest(query)); return responseConverter.sqlResponse(response); } catch (IOException e) { diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java index 216c68753..09e3b7646 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java @@ -68,6 +68,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -530,6 +531,22 @@ public co.elastic.clients.elasticsearch.indices.GetTemplateRequest indicesGetTem .of(gtr -> gtr.name(getTemplateRequest.getTemplateName()).flatSettings(true)); } + public co.elastic.clients.elasticsearch.sql.QueryRequest sqlQueryRequest(SqlQuery query) { + Assert.notNull(query, "Query must not be null."); + + return co.elastic.clients.elasticsearch.sql.QueryRequest.of(sqb -> { + sqb.query(query.getQuery()).catalog(query.getCatalog()).columnar(query.getColumnar()).cursor(query.getCursor()) + .fetchSize(query.getFetchSize()).fieldMultiValueLeniency(query.getFieldMultiValueLeniency()) + .indexUsingFrozen(query.getIndexIncludeFrozen()).keepAlive(time(query.getKeepAlive())) + .keepOnCompletion(query.getKeepOnCompletion()).pageTimeout(time(query.getPageTimeout())) + .requestTimeout(time(query.getRequestTimeout())) + .waitForCompletionTimeout(time(query.getWaitForCompletionTimeout())).filter(getQuery(query.getFilter(), null)) + .timeZone(Objects.toString(query.getTimeZone(), null)).format(Objects.toString(query.getFormat(), null)); + + return sqb; + }); + } + // endregion // region documents From 520bedf8f862f557baa4e8ebd281bf012d4fb2f9 Mon Sep 17 00:00:00 2001 From: Youssef Aouichaoui Date: Thu, 1 Aug 2024 13:51:57 +0200 Subject: [PATCH 3/6] Add the reactive version of the elasticsearch sql SQL client. Signed-off-by: Youssef Aouichaoui --- .../elc/ReactiveElasticsearchClient.java | 4 ++ .../elc/ReactiveElasticsearchSqlClient.java | 72 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchSqlClient.java diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClient.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClient.java index e998faf4b..040625a12 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClient.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchClient.java @@ -69,6 +69,10 @@ public ReactiveElasticsearchIndicesClient indices() { return new ReactiveElasticsearchIndicesClient(transport, transportOptions); } + public ReactiveElasticsearchSqlClient sql() { + return new ReactiveElasticsearchSqlClient(transport, transportOptions); + } + // endregion // region info diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchSqlClient.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchSqlClient.java new file mode 100644 index 000000000..442c5c5a9 --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchSqlClient.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.client.elc; + +import java.io.IOException; +import java.util.function.Function; + +import org.jetbrains.annotations.Nullable; + +import co.elastic.clients.ApiClient; +import co.elastic.clients.elasticsearch._types.ElasticsearchException; +import co.elastic.clients.elasticsearch.sql.QueryRequest; +import co.elastic.clients.elasticsearch.sql.QueryResponse; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.TransportOptions; +import co.elastic.clients.util.ObjectBuilder; +import reactor.core.publisher.Mono; + +/** + * Reactive version of {@link co.elastic.clients.elasticsearch.sql.ElasticsearchSqlClient}. + * + * @author Aouichaoui Youssef + * @since 5.4 + */ +public class ReactiveElasticsearchSqlClient extends ApiClient { + public ReactiveElasticsearchSqlClient(ElasticsearchTransport transport, @Nullable TransportOptions transportOptions) { + super(transport, transportOptions); + } + + @Override + public ReactiveElasticsearchSqlClient withTransportOptions(@Nullable TransportOptions transportOptions) { + return new ReactiveElasticsearchSqlClient(transport, transportOptions); + } + + /** + * Executes a SQL request + * + * @param fn a function that initializes a builder to create the {@link QueryRequest}. + */ + public final Mono query(Function> fn) + throws IOException, ElasticsearchException { + return query(fn.apply(new QueryRequest.Builder()).build()); + } + + /** + * Executes a SQL request. + */ + public Mono query(QueryRequest query) { + return Mono.fromFuture(transport.performRequestAsync(query, QueryRequest._ENDPOINT, transportOptions)); + } + + /** + * Executes a SQL request. + */ + public Mono query() { + return Mono.fromFuture( + transport.performRequestAsync(new QueryRequest.Builder().build(), QueryRequest._ENDPOINT, transportOptions)); + } +} From e8f0bf04d3f44aa844c381066fbad392e7f8ff3c Mon Sep 17 00:00:00 2001 From: Youssef Aouichaoui Date: Thu, 1 Aug 2024 13:53:47 +0200 Subject: [PATCH 4/6] Support for the reactive version of SQL queries. Signed-off-by: Youssef Aouichaoui --- .../elc/ReactiveElasticsearchTemplate.java | 15 +++ .../core/ReactiveElasticsearchOperations.java | 3 +- .../core/sql/ReactiveSqlOperations.java | 37 ++++++ .../elasticsearch/core/sql/SqlOperations.java | 7 + ...ReactiveSqlOperationsIntegrationTests.java | 124 ++++++++++++++++++ .../sql/SqlOperationsIntegrationTests.java | 7 +- 6 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperations.java create mode 100644 src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java index 5f4fd0360..b8a803e8c 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java @@ -17,6 +17,7 @@ import static co.elastic.clients.util.ApiTypeHelper.*; import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*; +import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.json; import co.elastic.clients.elasticsearch._types.Result; import co.elastic.clients.elasticsearch.core.*; @@ -25,6 +26,8 @@ import co.elastic.clients.json.JsonpMapper; import co.elastic.clients.transport.Version; import co.elastic.clients.transport.endpoints.BooleanResponse; +import org.springframework.data.elasticsearch.core.query.SqlQuery; +import org.springframework.data.elasticsearch.core.sql.SqlResponse; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -88,6 +91,7 @@ public class ReactiveElasticsearchTemplate extends AbstractReactiveElasticsearch private static final Log LOGGER = LogFactory.getLog(ReactiveElasticsearchTemplate.class); private final ReactiveElasticsearchClient client; + private final ReactiveElasticsearchSqlClient sqlClient; private final RequestConverter requestConverter; private final ResponseConverter responseConverter; private final JsonpMapper jsonpMapper; @@ -99,6 +103,7 @@ public ReactiveElasticsearchTemplate(ReactiveElasticsearchClient client, Elastic Assert.notNull(client, "client must not be null"); this.client = client; + this.sqlClient = client.sql(); this.jsonpMapper = client._transport().jsonpMapper(); requestConverter = new RequestConverter(converter, jsonpMapper); responseConverter = new ResponseConverter(jsonpMapper); @@ -646,6 +651,16 @@ public BaseQueryBuilder queryBuilderWithIds(List ids) { return NativeQuery.builder().withIds(ids); } + @Override + public Mono search(SqlQuery query) { + Assert.notNull(query, "Query must not be null."); + Assert.isTrue(query.getFormat() == null || json.equals(query.getFormat()), + "The Elasticsearch Java Client only supports JSON format."); + + co.elastic.clients.elasticsearch.sql.QueryRequest request = requestConverter.sqlQueryRequest(query); + return sqlClient.query(request).onErrorMap(this::translateException).map(responseConverter::sqlResponse); + } + /** * Callback interface to be used with {@link #execute(ReactiveElasticsearchTemplate.ClientCallback<>)} for operating * directly on {@link ReactiveElasticsearchClient}. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java index 7be0efe44..34679a9d5 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/ReactiveElasticsearchOperations.java @@ -21,6 +21,7 @@ import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; import org.springframework.data.elasticsearch.core.routing.RoutingResolver; import org.springframework.data.elasticsearch.core.script.ReactiveScriptOperations; +import org.springframework.data.elasticsearch.core.sql.ReactiveSqlOperations; import org.springframework.lang.Nullable; /** @@ -31,7 +32,7 @@ * @since 3.2 */ public interface ReactiveElasticsearchOperations - extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations { + extends ReactiveDocumentOperations, ReactiveSearchOperations, ReactiveScriptOperations, ReactiveSqlOperations { /** * Get the {@link ElasticsearchConverter} used. diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperations.java new file mode 100644 index 000000000..aaec1f6da --- /dev/null +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperations.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.sql; + +import org.springframework.data.elasticsearch.core.query.SqlQuery; + +import reactor.core.publisher.Mono; + +/** + * The reactive version of operations for the + * SQL search API. + * + * @author Aouichaoui Youssef + * @since 5.4 + */ +public interface ReactiveSqlOperations { + /** + * Execute the criteria {@code query} against elasticsearch and return result as {@link SqlResponse} + * + * @param query the query to execute + * @return {@link SqlResponse} containing the list of found objects + */ + Mono search(SqlQuery query); +} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java index c07d7b91c..4af636975 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java @@ -17,6 +17,13 @@ import org.springframework.data.elasticsearch.core.query.SqlQuery; +/** + * The operations for the + * SQL search API. + * + * @author Aouichaoui Youssef + * @since 5.4 + */ public interface SqlOperations { /** * Execute the criteria {@code query} against elasticsearch and return result as {@link SqlResponse} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java new file mode 100644 index 000000000..f20e1701e --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.elasticsearch.core.sql; + +import static org.springframework.data.elasticsearch.core.IndexOperationsAdapter.blocking; + +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.core.ReactiveElasticsearchOperations; +import org.springframework.data.elasticsearch.core.query.SqlQuery; +import org.springframework.data.elasticsearch.junit.jupiter.ReactiveElasticsearchTemplateConfiguration; +import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; + +import reactor.test.StepVerifier; + +/** + * Testing the reactive querying using SQL syntax. + * + * @author Youssef Aouichaoui + */ +@SpringIntegrationTest +@ContextConfiguration(classes = { ReactiveSqlOperationsIntegrationTests.Config.class }) +@DisplayName("Using Elasticsearch SQL Reactive Client") +public class ReactiveSqlOperationsIntegrationTests { + @Autowired ReactiveElasticsearchOperations operations; + + @BeforeEach + void setUp() { + // create index + blocking(operations.indexOps(EntityForSQL.class)).createWithMapping(); + + // add data + operations + .saveAll(List.of(EntityForSQL.builder().withViews(3).build(), EntityForSQL.builder().withViews(0).build()), + EntityForSQL.class) + .blockLast(); + } + + @AfterEach + void tearDown() { + // delete index + blocking(operations.indexOps(EntityForSQL.class)).delete(); + } + + // begin configuration region + @Configuration + @Import({ ReactiveElasticsearchTemplateConfiguration.class }) + static class Config {} + // end region + + @Test + void when_search_with_an_sql_query() { + // Given + SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").build(); + + // When + + // Then + operations.search(query).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + } + + // begin region + @Document(indexName = "entity_for_sql") + static class EntityForSQL { + @Id private String id; + private final Integer views; + + public EntityForSQL(EntityForSQL.Builder builder) { + this.views = builder.views; + } + + @Nullable + public String getId() { + return id; + } + + public Integer getViews() { + return views; + } + + public static EntityForSQL.Builder builder() { + return new EntityForSQL.Builder(); + } + + static class Builder { + private Integer views = 0; + + public EntityForSQL.Builder withViews(Integer views) { + this.views = views; + + return this; + } + + public EntityForSQL build() { + return new EntityForSQL(this); + } + } + } + // end region +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java index 8317f62e4..996ac7dd2 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java @@ -103,7 +103,6 @@ void when_search_with_an_sql_query_and_txt_format() { // begin region @Document(indexName = "entity_for_sql") static class EntityForSQL { - @Nullable @Id private String id; private final Integer views; @@ -116,10 +115,6 @@ public String getId() { return id; } - public void setId(@Nullable String id) { - this.id = id; - } - public Integer getViews() { return views; } @@ -129,7 +124,7 @@ public static Builder builder() { } static class Builder { - private Integer views; + private Integer views = 0; public Builder withViews(Integer views) { this.views = views; From ebe9a8f15222e0150bb28047a7a94dc11c2817ec Mon Sep 17 00:00:00 2001 From: Youssef Aouichaoui Date: Thu, 1 Aug 2024 14:15:31 +0200 Subject: [PATCH 5/6] Test aggregation with SQL. Signed-off-by: Youssef Aouichaoui --- .../data/elasticsearch/core/sql/SqlResponse.java | 5 +++++ .../core/sql/SqlOperationsIntegrationTests.java | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java index 20c1651ae..6f1a42df8 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java @@ -116,6 +116,11 @@ public Iterator> iterator() { return row.entrySet().iterator(); } + @Nullable + public JsonValue get(Column column) { + return row.get(column); + } + /** * This method should attempt to convert the SQL response into a document if the columns selected in an SQL query * correspond to the fields in the document. diff --git a/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java index 996ac7dd2..d07dee08f 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java @@ -15,6 +15,7 @@ */ package org.springframework.data.elasticsearch.core.sql; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -100,6 +101,20 @@ void when_search_with_an_sql_query_and_txt_format() { "The Elasticsearch Java Client only supports JSON format."); } + @Test + void when_search_with_an_sql_query_that_has_aggregated_column() { + // Given + SqlQuery query = SqlQuery.builder("SELECT SUM(views) AS TOTAL FROM entity_for_sql").build(); + + // When + + // Then + SqlResponse response = operations.search(query); + assertThat(response.getColumns()).first().extracting(SqlResponse.Column::name).isEqualTo("TOTAL"); + assertThat(response.getRows()).hasSize(1).first().extracting(row -> row.get(response.getColumns().get(0))) + .hasToString("3"); + } + // begin region @Document(indexName = "entity_for_sql") static class EntityForSQL { From e13d6f11da3f2801ba332a5ee5ff695c21e1bb2f Mon Sep 17 00:00:00 2001 From: Youssef Aouichaoui Date: Sun, 4 Aug 2024 10:52:45 +0200 Subject: [PATCH 6/6] Polishing. Signed-off-by: Youssef Aouichaoui --- .../client/elc/ElasticsearchTemplate.java | 3 - .../elc/ReactiveElasticsearchTemplate.java | 3 - .../client/elc/RequestConverter.java | 2 +- .../elasticsearch/core/query/SqlQuery.java | 65 ------------------- .../core/sql/ReactiveSqlOperations.java | 2 +- .../elasticsearch/core/sql/SqlOperations.java | 2 +- .../elasticsearch/core/sql/SqlResponse.java | 12 ---- .../core/sql/types/ResponseFormat.java | 29 --------- .../core/sql/types/package-info.java | 4 -- ...ReactiveSqlOperationsIntegrationTests.java | 2 +- .../sql/SqlOperationsIntegrationTests.java | 18 +---- 11 files changed, 6 insertions(+), 136 deletions(-) delete mode 100644 src/main/java/org/springframework/data/elasticsearch/core/sql/types/ResponseFormat.java delete mode 100644 src/main/java/org/springframework/data/elasticsearch/core/sql/types/package-info.java diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java index 8ef4413b6..49a0b5a00 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ElasticsearchTemplate.java @@ -16,7 +16,6 @@ package org.springframework.data.elasticsearch.client.elc; import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*; -import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.json; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.Time; @@ -667,8 +666,6 @@ public boolean deleteScript(String name) { @Override public SqlResponse search(SqlQuery query) { Assert.notNull(query, "Query must not be null."); - Assert.isTrue(query.getFormat() == null || json.equals(query.getFormat()), - "The Elasticsearch Java Client only supports JSON format."); try { QueryResponse response = sqlClient.query(requestConverter.sqlQueryRequest(query)); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java index b8a803e8c..aa18c0f74 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/ReactiveElasticsearchTemplate.java @@ -17,7 +17,6 @@ import static co.elastic.clients.util.ApiTypeHelper.*; import static org.springframework.data.elasticsearch.client.elc.TypeUtils.*; -import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.json; import co.elastic.clients.elasticsearch._types.Result; import co.elastic.clients.elasticsearch.core.*; @@ -654,8 +653,6 @@ public BaseQueryBuilder queryBuilderWithIds(List ids) { @Override public Mono search(SqlQuery query) { Assert.notNull(query, "Query must not be null."); - Assert.isTrue(query.getFormat() == null || json.equals(query.getFormat()), - "The Elasticsearch Java Client only supports JSON format."); co.elastic.clients.elasticsearch.sql.QueryRequest request = requestConverter.sqlQueryRequest(query); return sqlClient.query(request).onErrorMap(this::translateException).map(responseConverter::sqlResponse); diff --git a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java index 09e3b7646..6c3836701 100644 --- a/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/client/elc/RequestConverter.java @@ -541,7 +541,7 @@ public co.elastic.clients.elasticsearch.sql.QueryRequest sqlQueryRequest(SqlQuer .keepOnCompletion(query.getKeepOnCompletion()).pageTimeout(time(query.getPageTimeout())) .requestTimeout(time(query.getRequestTimeout())) .waitForCompletionTimeout(time(query.getWaitForCompletionTimeout())).filter(getQuery(query.getFilter(), null)) - .timeZone(Objects.toString(query.getTimeZone(), null)).format(Objects.toString(query.getFormat(), null)); + .timeZone(Objects.toString(query.getTimeZone(), null)).format("json"); return sqb; }); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java b/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java index 452ce9270..dbee7ef86 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/query/SqlQuery.java @@ -15,18 +15,11 @@ */ package org.springframework.data.elasticsearch.core.query; -import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.cbor; -import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.csv; -import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.json; -import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.smile; -import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.yaml; - import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.TimeZone; -import org.springframework.data.elasticsearch.core.sql.types.ResponseFormat; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -38,20 +31,6 @@ * @since 5.4 */ public class SqlQuery { - /** - * Separator for CSV results. - *

- * Default, this is set to {@code ,}. - */ - @Nullable private final String delimiter; - - /** - * The format for the response, such as csv, json, txt, can be viewed at the {@link ResponseFormat}. The java client - * of Elasticsearch only supports JSON. See {@link co.elastic.clients.transport.JsonEndpoint} - *

- * Default, this is set to {@code json}. - */ - @Nullable private final ResponseFormat format; /** * If true, returns partial results if there are shard request timeouts or shard failures. @@ -157,9 +136,6 @@ public class SqlQuery { @Nullable private final Duration waitForCompletionTimeout; private SqlQuery(Builder builder) { - this.delimiter = builder.delimiter; - this.format = builder.format; - this.allowPartialSearchResults = builder.allowPartialSearchResults; this.catalog = builder.catalog; @@ -185,16 +161,6 @@ private SqlQuery(Builder builder) { this.waitForCompletionTimeout = builder.waitForCompletionTimeout; } - @Nullable - public String getDelimiter() { - return delimiter; - } - - @Nullable - public ResponseFormat getFormat() { - return format; - } - @Nullable public Boolean getAllowPartialSearchResults() { return allowPartialSearchResults; @@ -279,9 +245,6 @@ public static Builder builder(String query) { } public static class Builder { - @Nullable private String delimiter; - @Nullable private ResponseFormat format; - @Nullable private Boolean allowPartialSearchResults; @Nullable private String catalog; @@ -313,24 +276,6 @@ private Builder(String query) { this.query = query; } - /** - * Separator for CSV results. - */ - public Builder withDelimiter(String delimiter) { - this.delimiter = delimiter; - - return this; - } - - /** - * The format for the response, such as csv, json, txt, can be viewed at the {@link ResponseFormat}. - */ - public Builder withFormat(ResponseFormat format) { - this.format = format; - - return this; - } - /** * If true, returns partial results if there are shard request timeouts or shard failures. */ @@ -482,16 +427,6 @@ public Builder withWaitForCompletionTimeout(Duration waitForCompletionTimeout) { } public SqlQuery build() { - if (Boolean.TRUE.equals(columnar)) { - if (!(cbor.equals(format) || json.equals(format) || smile.equals(format) || yaml.equals(format))) { - throw new IllegalArgumentException("Columnar format support only YAML, CBOR, JSON and SMILE."); - } - } - - if (delimiter != null && !csv.equals(format)) { - throw new IllegalArgumentException("Delimiter support is only available for CSV responses."); - } - return new SqlQuery(this); } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperations.java index aaec1f6da..6988026fa 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperations.java @@ -28,7 +28,7 @@ */ public interface ReactiveSqlOperations { /** - * Execute the criteria {@code query} against elasticsearch and return result as {@link SqlResponse} + * Execute the sql {@code query} against elasticsearch and return result as {@link SqlResponse} * * @param query the query to execute * @return {@link SqlResponse} containing the list of found objects diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java index 4af636975..3c1b3fc69 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlOperations.java @@ -26,7 +26,7 @@ */ public interface SqlOperations { /** - * Execute the criteria {@code query} against elasticsearch and return result as {@link SqlResponse} + * Execute the sql {@code query} against elasticsearch and return result as {@link SqlResponse} * * @param query the query to execute * @return {@link SqlResponse} containing the list of found objects diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java index 6f1a42df8..4436c419a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/sql/SqlResponse.java @@ -121,18 +121,6 @@ public JsonValue get(Column column) { return row.get(column); } - /** - * This method should attempt to convert the SQL response into a document if the columns selected in an SQL query - * correspond to the fields in the document. - * - * @param documentClass The class that represents the document. - * @return an instance of Document {@link T}. - * @param Document type. - */ - public T toDocument(Class documentClass) { - throw new UnsupportedOperationException("Not implemented yet."); - } - public static class Builder { private final Map row = new HashMap<>(); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/types/ResponseFormat.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/types/ResponseFormat.java deleted file mode 100644 index 4b9d0e8f5..000000000 --- a/src/main/java/org/springframework/data/elasticsearch/core/sql/types/ResponseFormat.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.elasticsearch.core.sql.types; - -public enum ResponseFormat { - // text - csv, - json, - tsv, - txt, - yaml, - - // binary - cbor, - smile -} diff --git a/src/main/java/org/springframework/data/elasticsearch/core/sql/types/package-info.java b/src/main/java/org/springframework/data/elasticsearch/core/sql/types/package-info.java deleted file mode 100644 index 335c5fe3c..000000000 --- a/src/main/java/org/springframework/data/elasticsearch/core/sql/types/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * SQL types and enums used across spring-data-elasticsearch. - */ -package org.springframework.data.elasticsearch.core.sql.types; diff --git a/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java index f20e1701e..8836894d5 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/sql/ReactiveSqlOperationsIntegrationTests.java @@ -72,7 +72,7 @@ void tearDown() { static class Config {} // end region - @Test + @Test // #2683 void when_search_with_an_sql_query() { // Given SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").build(); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java index d07dee08f..5367d00c0 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/sql/SqlOperationsIntegrationTests.java @@ -19,8 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.springframework.data.elasticsearch.core.sql.types.ResponseFormat.yaml; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -75,7 +73,7 @@ void tearDown() { static class Config {} // end region - @Test + @Test // #2683 void when_search_with_an_sql_query() { // Given SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").build(); @@ -89,19 +87,7 @@ void when_search_with_an_sql_query() { assertEquals(1, response.getRows().size()); } - @Test - void when_search_with_an_sql_query_and_txt_format() { - // Given - SqlQuery query = SqlQuery.builder("SELECT * FROM entity_for_sql WHERE views = 0").withFormat(yaml).build(); - - // When - - // Then - assertThrows(IllegalArgumentException.class, () -> operations.search(query), - "The Elasticsearch Java Client only supports JSON format."); - } - - @Test + @Test // #2683 void when_search_with_an_sql_query_that_has_aggregated_column() { // Given SqlQuery query = SqlQuery.builder("SELECT SUM(views) AS TOTAL FROM entity_for_sql").build();