From 9a2c06ee087eb1accbee5cb58004264b8e18fe76 Mon Sep 17 00:00:00 2001 From: v-jizhang Date: Fri, 2 Apr 2021 09:34:06 -0700 Subject: [PATCH] Add support for passthrough Elasticsearch queries Cherry-pick of https://github.com/trinodb/trino/pull/3735 This allows running queries over the results of a raw Elasticsearch query. It extends the syntax of the enhanced ES table names with the following: SELECT * FROM es.default."$query:" The query is base32-encoded to avoid having to deal with escaping quotes and case sensitivity issues in table identifiers. The result of these query tables is a table with a single row and a single column named "result" of type JSON. Co-authored-by: Martin Traverso Co-authored-by: Manfred Moser --- .../main/sphinx/connector/elasticsearch.rst | 22 +++++ presto-elasticsearch/pom.xml | 12 +++ .../elasticsearch/ElasticsearchMetadata.java | 90 ++++++++++++++++- .../ElasticsearchPageSourceProvider.java | 13 ++- .../ElasticsearchSplitManager.java | 17 +++- .../ElasticsearchTableHandle.java | 21 +++- .../PassthroughQueryPageSource.java | 99 +++++++++++++++++++ .../client/ElasticsearchClient.java | 32 ++++++ ...TestElasticsearchIntegrationSmokeTest.java | 31 ++++++ .../facebook/presto/spi/ColumnMetadata.java | 93 +++++++++++++++++ .../presto/spi/StandardErrorCode.java | 1 + 11 files changed, 419 insertions(+), 12 deletions(-) create mode 100644 presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/PassthroughQueryPageSource.java diff --git a/presto-docs/src/main/sphinx/connector/elasticsearch.rst b/presto-docs/src/main/sphinx/connector/elasticsearch.rst index 27f649df82089..03ad2a040dc10 100644 --- a/presto-docs/src/main/sphinx/connector/elasticsearch.rst +++ b/presto-docs/src/main/sphinx/connector/elasticsearch.rst @@ -290,6 +290,28 @@ as part of the table name, separated by a colon. For example: .. _full text query: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax +Pass-through Queries +-------------------- + +The Elasticsearch connector allows you to embed any valid Elasticsearch query, +that uses the `Elasticsearch Query DSL +`_ +in your SQL query. + +The results can then be used in any SQL statement, wrapping the Elasticsearch +query. The syntax extends the syntax of the enhanced Elasticsearch table names +with the following:: + + SELECT * FROM es.default."$query:" + +The Elasticsearch query string ``es-query`` is base32-encoded to avoid having to +deal with escaping quotes and case sensitivity issues in table identifiers. + +The result of these query tables is a table with a single row and a single +column named ``result`` of type VARCHAR. It contains the JSON payload returned +by Elasticsearch, and can be processed with the :doc:`built-in JSON functions +`. + AWS Authorization ----------------- diff --git a/presto-elasticsearch/pom.xml b/presto-elasticsearch/pom.xml index bb36be2c8f86a..7eeca7702b1fe 100644 --- a/presto-elasticsearch/pom.xml +++ b/presto-elasticsearch/pom.xml @@ -229,7 +229,19 @@ provided + + com.fasterxml.jackson.core + jackson-core + + + + org.jetbrains + annotations + 19.0.0 + test + + com.facebook.presto presto-client diff --git a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchMetadata.java b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchMetadata.java index f148fa4cbbc17..105e9ee2fff84 100644 --- a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchMetadata.java +++ b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchMetadata.java @@ -13,6 +13,7 @@ */ package com.facebook.presto.elasticsearch; +import com.facebook.airlift.json.JsonObjectMapperProvider; import com.facebook.presto.common.type.ArrayType; import com.facebook.presto.common.type.RowType; import com.facebook.presto.common.type.StandardTypes; @@ -33,11 +34,15 @@ import com.facebook.presto.spi.ConnectorTableLayoutResult; import com.facebook.presto.spi.ConnectorTableMetadata; import com.facebook.presto.spi.Constraint; +import com.facebook.presto.spi.PrestoException; import com.facebook.presto.spi.SchemaTableName; import com.facebook.presto.spi.SchemaTablePrefix; import com.facebook.presto.spi.connector.ConnectorMetadata; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; import javax.inject.Inject; @@ -58,14 +63,25 @@ import static com.facebook.presto.common.type.TinyintType.TINYINT; import static com.facebook.presto.common.type.VarbinaryType.VARBINARY; import static com.facebook.presto.common.type.VarcharType.VARCHAR; +import static com.facebook.presto.elasticsearch.ElasticsearchTableHandle.Type.QUERY; +import static com.facebook.presto.elasticsearch.ElasticsearchTableHandle.Type.SCAN; +import static com.facebook.presto.spi.StandardErrorCode.INVALID_ARGUMENTS; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Locale.ENGLISH; import static java.util.Objects.requireNonNull; public class ElasticsearchMetadata implements ConnectorMetadata { + private static final ObjectMapper JSON_PARSER = new JsonObjectMapperProvider().get(); + + private static final String PASSTHROUGH_QUERY_SUFFIX = "$query"; + private final Map queryTableColumns; + private final ColumnMetadata queryResultColumnMetadata; + private final ElasticsearchClient client; private final String schemaName; private final Type ipAddressType; @@ -76,7 +92,18 @@ public ElasticsearchMetadata(TypeManager typeManager, ElasticsearchClient client requireNonNull(config, "config is null"); this.ipAddressType = typeManager.getType(new TypeSignature(StandardTypes.IPADDRESS)); this.client = requireNonNull(client, "client is null"); + requireNonNull(config, "config is null"); this.schemaName = config.getDefaultSchema(); + + Type jsonType = typeManager.getType(new TypeSignature(StandardTypes.JSON)); + queryResultColumnMetadata = ColumnMetadata.builder() + .setName("result") + .setType(jsonType) + .setNullable(true) + .setHidden(false) + .build(); + + queryTableColumns = ImmutableMap.of("result", new ElasticsearchColumnHandle("result", jsonType, false)); } @Override @@ -93,12 +120,37 @@ public ElasticsearchTableHandle getTableHandle(ConnectorSession session, SchemaT String[] parts = tableName.getTableName().split(":", 2); String table = parts[0]; Optional query = Optional.empty(); + ElasticsearchTableHandle.Type type = SCAN; if (parts.length == 2) { - query = Optional.of(parts[1]); + if (table.endsWith(PASSTHROUGH_QUERY_SUFFIX)) { + table = table.substring(0, table.length() - PASSTHROUGH_QUERY_SUFFIX.length()); + byte[] decoded; + try { + decoded = BaseEncoding.base32().decode(parts[1].toUpperCase(ENGLISH)); + } + catch (IllegalArgumentException e) { + throw new PrestoException(INVALID_ARGUMENTS, format("Elasticsearch query for '%s' is not base32-encoded correctly", table), e); + } + + String queryJson = new String(decoded, UTF_8); + try { + // Ensure this is valid json + JSON_PARSER.readTree(queryJson); + } + catch (JsonProcessingException e) { + throw new PrestoException(INVALID_ARGUMENTS, format("Elasticsearch query for '%s' is not valid JSON", table), e); + } + + query = Optional.of(queryJson); + type = QUERY; + } + else { + query = Optional.of(parts[1]); + } } if (listTables(session, Optional.of(schemaName)).contains(new SchemaTableName(schemaName, table))) { - return new ElasticsearchTableHandle(schemaName, table, query); + return new ElasticsearchTableHandle(type, schemaName, table, query); } } return null; @@ -122,6 +174,12 @@ public ConnectorTableLayout getTableLayout(ConnectorSession session, ConnectorTa public ConnectorTableMetadata getTableMetadata(ConnectorSession session, ConnectorTableHandle table) { ElasticsearchTableHandle handle = (ElasticsearchTableHandle) table; + + if (isPassthroughQuery(handle)) { + return new ConnectorTableMetadata( + new SchemaTableName(handle.getSchema(), handle.getIndex()), + ImmutableList.of(queryResultColumnMetadata)); + } return getTableMetadata(handle.getSchema(), handle.getIndex()); } @@ -295,6 +353,12 @@ public List listTables(ConnectorSession session, Optional getColumnHandles(ConnectorSession session, ConnectorTableHandle tableHandle) { + ElasticsearchTableHandle table = (ElasticsearchTableHandle) tableHandle; + + if (isPassthroughQuery(table)) { + return queryTableColumns; + } + InternalTableMetadata tableMetadata = makeInternalTableMetadata(tableHandle); return tableMetadata.getColumnHandles(); } @@ -302,8 +366,26 @@ public Map getColumnHandles(ConnectorSession session, Conn @Override public ColumnMetadata getColumnMetadata(ConnectorSession session, ConnectorTableHandle tableHandle, ColumnHandle columnHandle) { - ElasticsearchColumnHandle handle = (ElasticsearchColumnHandle) columnHandle; - return new ColumnMetadata(handle.getName(), handle.getType()); + ElasticsearchTableHandle table = (ElasticsearchTableHandle) tableHandle; + ElasticsearchColumnHandle column = (ElasticsearchColumnHandle) columnHandle; + + if (isPassthroughQuery(table)) { + if (column.getName().equals(queryResultColumnMetadata.getName())) { + return queryResultColumnMetadata; + } + + throw new IllegalArgumentException(format("Unexpected column for table '%s$query': %s", table.getIndex(), column.getName())); + } + + return ColumnMetadata.builder() + .setName(column.getName()) + .setType(column.getType()) + .build(); + } + + private static boolean isPassthroughQuery(ElasticsearchTableHandle table) + { + return table.getType().equals(QUERY); } @Override diff --git a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchPageSourceProvider.java b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchPageSourceProvider.java index 50c197e013adf..82c8ce7e5a696 100644 --- a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchPageSourceProvider.java +++ b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchPageSourceProvider.java @@ -13,6 +13,10 @@ */ package com.facebook.presto.elasticsearch; +import com.facebook.presto.common.type.StandardTypes; +import com.facebook.presto.common.type.Type; +import com.facebook.presto.common.type.TypeManager; +import com.facebook.presto.common.type.TypeSignature; import com.facebook.presto.elasticsearch.client.ElasticsearchClient; import com.facebook.presto.spi.ColumnHandle; import com.facebook.presto.spi.ConnectorPageSource; @@ -27,6 +31,7 @@ import java.util.List; +import static com.facebook.presto.elasticsearch.ElasticsearchTableHandle.Type.QUERY; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.Objects.requireNonNull; @@ -34,11 +39,13 @@ public class ElasticsearchPageSourceProvider implements ConnectorPageSourceProvider { private final ElasticsearchClient client; + private final Type jsonType; @Inject - public ElasticsearchPageSourceProvider(ElasticsearchClient client) + public ElasticsearchPageSourceProvider(ElasticsearchClient client, TypeManager typeManager) { this.client = requireNonNull(client, "client is null"); + this.jsonType = typeManager.getType(new TypeSignature(StandardTypes.JSON)); } @Override @@ -55,6 +62,10 @@ public ConnectorPageSource createPageSource( ElasticsearchTableLayoutHandle layoutHandle = (ElasticsearchTableLayoutHandle) layout; ElasticsearchSplit elasticsearchSplit = (ElasticsearchSplit) split; + if (layoutHandle.getTable().getType().equals(QUERY)) { + return new PassthroughQueryPageSource(client, layoutHandle.getTable(), jsonType); + } + if (columns.isEmpty()) { return new CountQueryPageSource(client, session, layoutHandle.getTable(), elasticsearchSplit); } diff --git a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchSplitManager.java b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchSplitManager.java index a4cd0fa9b44ae..be0c5d48ddb1b 100644 --- a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchSplitManager.java +++ b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchSplitManager.java @@ -20,11 +20,14 @@ import com.facebook.presto.spi.FixedSplitSource; import com.facebook.presto.spi.connector.ConnectorSplitManager; import com.facebook.presto.spi.connector.ConnectorTransactionHandle; +import com.google.common.collect.ImmutableList; import javax.inject.Inject; import java.util.List; +import java.util.Optional; +import static com.facebook.presto.elasticsearch.ElasticsearchTableHandle.Type.QUERY; import static com.google.common.collect.ImmutableList.toImmutableList; import static java.util.Objects.requireNonNull; @@ -49,10 +52,14 @@ public ConnectorSplitSource getSplits( ElasticsearchTableLayoutHandle layoutHandle = (ElasticsearchTableLayoutHandle) layout; ElasticsearchTableHandle tableHandle = layoutHandle.getTable(); - List splits = client.getSearchShards(tableHandle.getIndex()).stream() - .map(shard -> new ElasticsearchSplit(shard.getIndex(), shard.getId(), layoutHandle.getTupleDomain(), shard.getAddress())) - .collect(toImmutableList()); - - return new FixedSplitSource(splits); + if (tableHandle.getType().equals(QUERY)) { + return new FixedSplitSource(ImmutableList.of(new ElasticsearchSplit(tableHandle.getIndex(), 0, layoutHandle.getTupleDomain(), Optional.empty()))); + } + else { + List splits = client.getSearchShards(tableHandle.getIndex()).stream() + .map(shard -> new ElasticsearchSplit(shard.getIndex(), shard.getId(), layoutHandle.getTupleDomain(), shard.getAddress())) + .collect(toImmutableList()); + return new FixedSplitSource(splits); + } } } diff --git a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchTableHandle.java b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchTableHandle.java index b27341ef24439..48d9a37c82794 100644 --- a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchTableHandle.java +++ b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/ElasticsearchTableHandle.java @@ -26,21 +26,36 @@ public final class ElasticsearchTableHandle implements ConnectorTableHandle { + public enum Type + { + SCAN, + QUERY + } + + private final Type type; private final String schema; private final String index; private final Optional query; @JsonCreator public ElasticsearchTableHandle( + @JsonProperty("type") Type type, @JsonProperty("schema") String schema, @JsonProperty("index") String index, @JsonProperty("query") Optional query) { + this.type = requireNonNull(type, "type is null"); this.schema = requireNonNull(schema, "schema is null"); this.index = requireNonNull(index, "index is null"); this.query = requireNonNull(query, "query is null"); } + @JsonProperty + public Type getType() + { + return type; + } + @JsonProperty public String getIndex() { @@ -62,7 +77,7 @@ public Optional getQuery() @Override public int hashCode() { - return Objects.hash(schema, index, query); + return Objects.hash(type, schema, index, query); } @Override @@ -76,7 +91,8 @@ public boolean equals(Object obj) } ElasticsearchTableHandle other = (ElasticsearchTableHandle) obj; - return Objects.equals(this.getSchema(), other.getSchema()) && + return Objects.equals(this.type, other.getType()) && + Objects.equals(this.getSchema(), other.getSchema()) && Objects.equals(this.getIndex(), other.getIndex()) && Objects.equals(this.getQuery(), other.getQuery()); } @@ -85,6 +101,7 @@ public boolean equals(Object obj) public String toString() { return toStringHelper(this) + .add("type", getType()) .add("schema", getSchema()) .add("index", getIndex()) .add("query", getQuery()) diff --git a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/PassthroughQueryPageSource.java b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/PassthroughQueryPageSource.java new file mode 100644 index 0000000000000..e4438919a4d9e --- /dev/null +++ b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/PassthroughQueryPageSource.java @@ -0,0 +1,99 @@ +/* + * 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 + * + * http://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 com.facebook.presto.elasticsearch; + +import com.facebook.presto.common.Page; +import com.facebook.presto.common.PageBuilder; +import com.facebook.presto.common.block.BlockBuilder; +import com.facebook.presto.common.type.Type; +import com.facebook.presto.elasticsearch.client.ElasticsearchClient; +import com.facebook.presto.spi.ConnectorPageSource; +import com.google.common.collect.ImmutableList; +import io.airlift.slice.Slices; + +import java.io.IOException; + +import static java.util.Objects.requireNonNull; + +public class PassthroughQueryPageSource + implements ConnectorPageSource +{ + private final long readTimeNanos; + private final String result; + private final Type jsonType; + private boolean done; + + public PassthroughQueryPageSource(ElasticsearchClient client, ElasticsearchTableHandle table, Type jsonType) + { + requireNonNull(client, "client is null"); + requireNonNull(table, "table is null"); + this.jsonType = requireNonNull(jsonType, "jsonType is null"); + + long start = System.nanoTime(); + result = client.executeQuery(table.getIndex(), table.getQuery().get()); + readTimeNanos = System.nanoTime() - start; + } + + @Override + public long getCompletedBytes() + { + return result.length(); + } + + @Override + public long getCompletedPositions() + { + return 0; + } + + @Override + public long getReadTimeNanos() + { + return readTimeNanos; + } + + @Override + public boolean isFinished() + { + return done; + } + + @Override + public Page getNextPage() + { + if (done) { + return null; + } + + done = true; + + PageBuilder page = new PageBuilder(1, ImmutableList.of(jsonType)); + page.declarePosition(); + BlockBuilder column = page.getBlockBuilder(0); + jsonType.writeSlice(column, Slices.utf8Slice(result)); + return page.build(); + } + + @Override + public long getSystemMemoryUsage() + { + return 0; + } + + @Override + public void close() + throws IOException + { + } +} diff --git a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/client/ElasticsearchClient.java b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/client/ElasticsearchClient.java index 804cde832b0c3..b3a751d522987 100644 --- a/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/client/ElasticsearchClient.java +++ b/presto-elasticsearch/src/main/java/com/facebook/presto/elasticsearch/client/ElasticsearchClient.java @@ -37,6 +37,7 @@ import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; import org.apache.http.impl.nio.reactor.IOReactorConfig; @@ -102,6 +103,7 @@ import static com.google.common.collect.ImmutableMap.toImmutableMap; import static java.lang.StrictMath.toIntExact; import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.list; import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; import static org.elasticsearch.action.search.SearchType.QUERY_THEN_FETCH; @@ -537,6 +539,36 @@ private JsonNode nullSafeNode(JsonNode jsonNode, String name) return jsonNode.get(name); } + public String executeQuery(String index, String query) + { + String path = format("/%s/_search", index); + + Response response; + try { + response = client.getLowLevelClient() + .performRequest( + "GET", + path, + ImmutableMap.of(), + new ByteArrayEntity(query.getBytes(UTF_8)), + new BasicHeader("Content-Type", "application/json"), + new BasicHeader("Accept-Encoding", "application/json")); + } + catch (IOException e) { + throw new PrestoException(ELASTICSEARCH_CONNECTION_ERROR, e); + } + + String body; + try { + body = EntityUtils.toString(response.getEntity()); + } + catch (IOException e) { + throw new PrestoException(ELASTICSEARCH_INVALID_RESPONSE, e); + } + + return body; + } + public SearchResponse beginSearch(String index, int shard, QueryBuilder query, Optional> fields, List documentFields, Optional sort) { SearchSourceBuilder sourceBuilder = SearchSourceBuilder.searchSource() diff --git a/presto-elasticsearch/src/test/java/com/facebook/presto/elasticsearch/TestElasticsearchIntegrationSmokeTest.java b/presto-elasticsearch/src/test/java/com/facebook/presto/elasticsearch/TestElasticsearchIntegrationSmokeTest.java index 7f1b457817f47..f103d868b91e8 100644 --- a/presto-elasticsearch/src/test/java/com/facebook/presto/elasticsearch/TestElasticsearchIntegrationSmokeTest.java +++ b/presto-elasticsearch/src/test/java/com/facebook/presto/elasticsearch/TestElasticsearchIntegrationSmokeTest.java @@ -19,10 +19,12 @@ import com.facebook.presto.tests.AbstractTestIntegrationSmokeTest; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.io.BaseEncoding; import com.google.common.io.Closer; import io.airlift.tpch.TpchTable; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesRequest; import org.elasticsearch.common.xcontent.XContentType; +import org.intellij.lang.annotations.Language; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -38,6 +40,7 @@ import static com.facebook.presto.testing.MaterializedResult.resultBuilder; import static com.facebook.presto.testing.assertions.Assert.assertEquals; import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.elasticsearch.client.Requests.indexAliasesRequest; import static org.elasticsearch.client.Requests.refreshRequest; @@ -842,4 +845,32 @@ private void index(String indexName, Map document) .setSource(document) .get(); } + + @Test + public void testPassthroughQuery() + { + @Language("JSON") + String query = "{\n" + + " \"size\": 0,\n" + + " \"aggs\" : {\n" + + " \"max_orderkey\" : { \"max\" : { \"field\" : \"orderkey\" } },\n" + + " \"sum_orderkey\" : { \"sum\" : { \"field\" : \"orderkey\" } }\n" + + " }\n" + + "}"; + + assertQuery( + format("WITH data(r) AS (" + + " SELECT CAST(result AS ROW(aggregations ROW(max_orderkey ROW(value BIGINT), sum_orderkey ROW(value BIGINT)))) " + + " FROM \"orders$query:%s\") " + + "SELECT r.aggregations.max_orderkey.value, r.aggregations.sum_orderkey.value " + + "FROM data", BaseEncoding.base32().encode(query.getBytes(UTF_8))), + "VALUES (60000, 449872500)"); + + assertQueryFails( + "SELECT * FROM \"orders$query:invalid-base32-encoding\"", + "Elasticsearch query for 'orders' is not base32-encoded correctly"); + assertQueryFails( + format("SELECT * FROM \"orders$query:%s\"", BaseEncoding.base32().encode("invalid json".getBytes(UTF_8))), + "Elasticsearch query for 'orders' is not valid JSON"); + } } diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/ColumnMetadata.java b/presto-spi/src/main/java/com/facebook/presto/spi/ColumnMetadata.java index 2f8a2addc8bcf..8d26f39cd0f8e 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/ColumnMetadata.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/ColumnMetadata.java @@ -15,9 +15,12 @@ import com.facebook.presto.common.type.Type; +import javax.annotation.Nullable; + import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; import static com.facebook.presto.spi.SchemaUtil.checkNotEmpty; import static java.util.Collections.emptyMap; @@ -40,21 +43,37 @@ public ColumnMetadata(String name, Type type) this(name, type, true, null, null, false, emptyMap()); } + /** + * @deprecated Use {@link #builder()} instead. + */ + @Deprecated public ColumnMetadata(String name, Type type, String comment, boolean hidden) { this(name, type, true, comment, null, hidden, emptyMap()); } + /** + * @deprecated Use {@link #builder()} instead. + */ + @Deprecated public ColumnMetadata(String name, Type type, String comment, String extraInfo, boolean hidden) { this(name, type, true, comment, extraInfo, hidden, emptyMap()); } + /** + * @deprecated Use {@link #builder()} instead. + */ + @Deprecated public ColumnMetadata(String name, Type type, String comment, String extraInfo, boolean hidden, Map properties) { this(name, type, true, comment, extraInfo, hidden, properties); } + /** + * @deprecated Use {@link #builder()} instead. + */ + @Deprecated public ColumnMetadata(String name, Type type, boolean nullable, String comment, String extraInfo, boolean hidden, Map properties) { checkNotEmpty(name, "name"); @@ -85,11 +104,13 @@ public boolean isNullable() return nullable; } + @Nullable // TODO make it Optional public String getComment() { return comment; } + @Nullable // TODO make it Optional public String getExtraInfo() { return extraInfo; @@ -151,4 +172,76 @@ public boolean equals(Object obj) Objects.equals(this.extraInfo, other.extraInfo) && Objects.equals(this.hidden, other.hidden); } + + public static Builder builder() + { + return new Builder(); + } + + public static class Builder + { + private String name; + private Type type; + private boolean nullable = true; + private Optional comment = Optional.empty(); + private Optional extraInfo = Optional.empty(); + private boolean hidden; + private Map properties = emptyMap(); + + private Builder() {} + + public Builder setName(String name) + { + this.name = requireNonNull(name, "name is null"); + return this; + } + + public Builder setType(Type type) + { + this.type = requireNonNull(type, "type is null"); + return this; + } + + public Builder setNullable(boolean nullable) + { + this.nullable = nullable; + return this; + } + + public Builder setComment(Optional comment) + { + this.comment = requireNonNull(comment, "comment is null"); + return this; + } + + public Builder setExtraInfo(Optional extraInfo) + { + this.extraInfo = requireNonNull(extraInfo, "extraInfo is null"); + return this; + } + + public Builder setHidden(boolean hidden) + { + this.hidden = hidden; + return this; + } + + public Builder setProperties(Map properties) + { + this.properties = requireNonNull(properties, "properties is null"); + return this; + } + + public ColumnMetadata build() + { + return new ColumnMetadata( + name, + type, + nullable, + comment.orElse(null), + extraInfo.orElse(null), + hidden, + properties); + } + } } diff --git a/presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java b/presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java index 9d786b9c4eb30..39d52fa67b346 100644 --- a/presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java +++ b/presto-spi/src/main/java/com/facebook/presto/spi/StandardErrorCode.java @@ -65,6 +65,7 @@ public enum StandardErrorCode INVALID_ANALYZE_PROPERTY(0x0000_002A, USER_ERROR), GENERATED_BYTECODE_TOO_LARGE(0x0000_002B, USER_ERROR), WARNING_AS_ERROR(0x0000_002C, USER_ERROR), + INVALID_ARGUMENTS(0x0000_002D, USER_ERROR), GENERIC_INTERNAL_ERROR(0x0001_0000, INTERNAL_ERROR), TOO_MANY_REQUESTS_FAILED(0x0001_0001, INTERNAL_ERROR),