From 8e478145e295c40b34ed9abb8b74ade4108d2d0d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 13 Mar 2024 17:07:33 +0100 Subject: [PATCH] Add ES|QL helpers (#762) (#763) Adds helpers for ES|QL. The ES|QL result format is meant to be compact: it is composed of a metadata part giving field names and their types and a 2D array of values, which isn't easy to use in application code. This PR provides adapters that convert the ES|QL JSON result format into higher level types that are easier to use. Two adapters are provided: * An `ObjectsAdatper` that combines field names and values from the array to build a collection of objects using JSON to object mapping * A `ResultSetAdpater` that provides an implementation of the well-known JDBC `ResultSet`. This is a cursor-based API where the application can inspect at runtime the type and names of the ES|QL results, and is therefore more suited for ad-hoc or dynamic queries where the result structure isn't known in advance. Along with adapters, additional methods in `ElasticsearchEsqlClient` provide simple way to send queries using just a string and optional parameters when you don't need to specify additional request details. Co-authored-by: Sylvain Wallez --- .../_helpers/esql/EsqlAdapter.java | 47 + .../_helpers/esql/EsqlAdapterBase.java | 69 + .../_helpers/esql/EsqlHelper.java | 114 ++ .../_helpers/esql/EsqlMetadata.java | 77 + .../_helpers/esql/ObjectsEsqlAdapter.java | 110 ++ .../_helpers/esql/jdbc/Cursor.java | 48 + .../_helpers/esql/jdbc/EsType.java | 86 ++ .../_helpers/esql/jdbc/ExtraTypes.java | 47 + .../_helpers/esql/jdbc/JdbcColumnInfo.java | 111 ++ .../_helpers/esql/jdbc/JdbcDateUtils.java | 97 ++ .../_helpers/esql/jdbc/JdbcResultSet.java | 1274 +++++++++++++++++ .../esql/jdbc/JdbcResultSetMetaData.java | 174 +++ .../_helpers/esql/jdbc/JdbcWrapper.java | 41 + .../_helpers/esql/jdbc/JsonpCursor.java | 146 ++ .../esql/jdbc/ResultSetEsqlAdapter.java | 57 + .../_helpers/esql/jdbc/StringUtils.java | 169 +++ .../_helpers/esql/jdbc/TypeConverter.java | 681 +++++++++ .../_helpers/esql/jdbc/TypeUtils.java | 147 ++ .../_types/ElasticsearchException.java | 3 +- .../elasticsearch/_types/FieldValue.java | 1 - .../esql/ElasticsearchEsqlAsyncClient.java | 41 + .../esql/ElasticsearchEsqlClient.java | 42 + .../esql/EsqlAdapterEndToEndTest.java | 194 +++ .../_helpers/esql/EsqlAdapterTest.java | 91 ++ .../_helpers/esql/employees.ndjson | 201 +++ 25 files changed, 4066 insertions(+), 2 deletions(-) create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapter.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterBase.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlHelper.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlMetadata.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/ObjectsEsqlAdapter.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/Cursor.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/EsType.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/ExtraTypes.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcColumnInfo.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcDateUtils.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcResultSet.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcResultSetMetaData.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcWrapper.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JsonpCursor.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/ResultSetEsqlAdapter.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/StringUtils.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/TypeConverter.java create mode 100644 java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/TypeUtils.java create mode 100644 java-client/src/test/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterEndToEndTest.java create mode 100644 java-client/src/test/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterTest.java create mode 100644 java-client/src/test/resources/co/elastic/clients/elasticsearch/_helpers/esql/employees.ndjson diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapter.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapter.java new file mode 100644 index 000000000..0be4ad393 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapter.java @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql; + +import co.elastic.clients.ApiClient; +import co.elastic.clients.elasticsearch.esql.QueryRequest; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.endpoints.BinaryResponse; + +import java.io.IOException; + +/** + * A deserializer for ES|QL responses. + */ +public interface EsqlAdapter { + /** + * ESQL result format this deserializer accepts (text, csv, json, arrow, etc.) + */ + String format(); + + /** + * For JSON results, whether the result should be organized in rows or columns + */ + boolean columnar(); + + /** + * Deserialize the raw http response returned by the server + */ + Result deserialize(ApiClient client, QueryRequest request, BinaryResponse response) throws IOException; +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterBase.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterBase.java new file mode 100644 index 000000000..374de7f07 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterBase.java @@ -0,0 +1,69 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql; + +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.JsonpMappingException; +import co.elastic.clients.json.JsonpUtils; +import jakarta.json.stream.JsonParser; + +import java.util.List; + +public abstract class EsqlAdapterBase implements EsqlAdapter { + + /** + * Reads the header of an ES|QL response, moving the parser at the beginning of the first value row. + * The caller can then read row arrays until finding an end array that closes the top-level array. + */ + public static EsqlMetadata readHeader(JsonParser parser, JsonpMapper mapper) { + JsonpUtils.expectNextEvent(parser, JsonParser.Event.START_OBJECT); + JsonpUtils.expectNextEvent(parser, JsonParser.Event.KEY_NAME); + + if (!"columns".equals(parser.getString())) { + throw new JsonpMappingException("Expecting a 'columns' property, but found '" + parser.getString() + "'", parser.getLocation()); + } + + List columns = JsonpDeserializer + .arrayDeserializer(EsqlMetadata.EsqlColumn._DESERIALIZER) + .deserialize(parser, mapper); + + EsqlMetadata result = new EsqlMetadata(); + result.columns = columns; + + JsonpUtils.expectNextEvent(parser, JsonParser.Event.KEY_NAME); + + if (!"values".equals(parser.getString())) { + throw new JsonpMappingException("Expecting a 'values' property, but found '" + parser.getString() + "'", parser.getLocation()); + } + + JsonpUtils.expectNextEvent(parser, JsonParser.Event.START_ARRAY); + + return result; + } + + /** + * Checks the footer of an ES|QL response, once the values have been read. + */ + public static void readFooter(JsonParser parser) { + JsonpUtils.expectNextEvent(parser, JsonParser.Event.END_OBJECT); + } + +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlHelper.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlHelper.java new file mode 100644 index 000000000..a57afcdb3 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlHelper.java @@ -0,0 +1,114 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql; + +import co.elastic.clients.elasticsearch._types.FieldValue; +import co.elastic.clients.elasticsearch.esql.ElasticsearchEsqlAsyncClient; +import co.elastic.clients.elasticsearch.esql.ElasticsearchEsqlClient; +import co.elastic.clients.elasticsearch.esql.QueryRequest; +import co.elastic.clients.json.JsonData; +import co.elastic.clients.transport.endpoints.BinaryResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class EsqlHelper { + + //----- Synchronous + + public static T query( + ElasticsearchEsqlClient client, EsqlAdapter adapter, String query, Object... params + ) throws IOException { + QueryRequest request = buildRequest(adapter, query, params); + BinaryResponse response = client.query(request); + return adapter.deserialize(client, request, response); + } + + public static T query(ElasticsearchEsqlClient client, EsqlAdapter adapter, QueryRequest request) throws IOException { + request = buildRequest(adapter, request); + BinaryResponse response = client.query(request); + return adapter.deserialize(client, request, response); + } + + //----- Asynchronous + + public static CompletableFuture queryAsync( + ElasticsearchEsqlAsyncClient client, EsqlAdapter adapter, String query, Object... params + ) { + return doQueryAsync(client, adapter, buildRequest(adapter, query, params)); + } + + public static CompletableFuture queryAsync( + ElasticsearchEsqlAsyncClient client, EsqlAdapter adapter, QueryRequest request + ) { + return doQueryAsync(client, adapter, buildRequest(adapter, request)); + } + + private static CompletableFuture doQueryAsync( + ElasticsearchEsqlAsyncClient client, EsqlAdapter adapter, QueryRequest request + ) { + return client + .query(request) + .thenApply(r -> { + try { + return adapter.deserialize(client, request, r); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + //----- Utilities + + private static QueryRequest buildRequest(EsqlAdapter adapter, String query, Object... params) { + return QueryRequest.of(esql -> esql + .format(adapter.format()) + .columnar(adapter.columnar()) + .query(query) + .params(asFieldValues(params)) + ); + } + + private static QueryRequest buildRequest(EsqlAdapter adapter, QueryRequest request) { + return QueryRequest.of(q -> q + // Set/override format and columnar + .format(adapter.format()) + .columnar(adapter.columnar()) + + .delimiter(request.delimiter()) + .filter(request.filter()) + .locale(request.locale()) + .params(request.params()) + .query(request.query()) + ); + } + + private static List asFieldValues(Object... objects) { + + List result = new ArrayList<>(objects.length); + for (Object object: objects) { + result.add(FieldValue.of(JsonData.of(object))); + } + + return result; + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlMetadata.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlMetadata.java new file mode 100644 index 000000000..d4e23d00b --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlMetadata.java @@ -0,0 +1,77 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql; + +import co.elastic.clients.json.JsonpDeserializable; +import co.elastic.clients.json.JsonpDeserializer; +import co.elastic.clients.json.ObjectBuilderDeserializer; +import co.elastic.clients.json.ObjectDeserializer; +import co.elastic.clients.util.ObjectBuilder; +import co.elastic.clients.util.ObjectBuilderBase; + +import java.util.List; + +public class EsqlMetadata { + + @JsonpDeserializable + public static class EsqlColumn { + private String name; + private String type; + + public String name() { + return name; + } + + public String type() { + return type; + } + + public static class Builder extends ObjectBuilderBase implements ObjectBuilder { + EsqlColumn object = new EsqlColumn(); + + public Builder name(String value) { + object.name = value; + return this; + } + + public Builder type(String value) { + object.type = value; + return this; + } + + @Override + public EsqlColumn build() { + _checkSingleUse(); + return object; + } + } + + public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( + EsqlColumn.Builder::new, EsqlColumn::setupEsqlColumnDeserializer + ); + + protected static void setupEsqlColumnDeserializer(ObjectDeserializer op) { + op.add(EsqlColumn.Builder::name, JsonpDeserializer.stringDeserializer(), "name"); + op.add(EsqlColumn.Builder::type, JsonpDeserializer.stringDeserializer(), "type"); + } + } + + public List columns; +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/ObjectsEsqlAdapter.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/ObjectsEsqlAdapter.java new file mode 100644 index 000000000..9de683553 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/ObjectsEsqlAdapter.java @@ -0,0 +1,110 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql; + +import co.elastic.clients.ApiClient; +import co.elastic.clients.elasticsearch.esql.QueryRequest; +import co.elastic.clients.json.BufferingJsonGenerator; +import co.elastic.clients.json.BufferingJsonpMapper; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.JsonpUtils; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.endpoints.BinaryResponse; +import jakarta.json.stream.JsonParser; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * And ES|QL adapter that returns an iterable of domain objects + */ +class ObjectsEsqlAdapter implements EsqlAdapter> { + + public static ObjectsEsqlAdapter of(Class clazz) { + return new ObjectsEsqlAdapter<>(clazz); + } + + public static ObjectsEsqlAdapter of(Type type) { + return new ObjectsEsqlAdapter<>(type); + } + + private final Type type; + + ObjectsEsqlAdapter(Type type) { + this.type = type; + } + + @Override + public String format() { + return "json"; + } + + @Override + public boolean columnar() { + return false; + } + + @Override + public Iterable deserialize(ApiClient client, QueryRequest request, BinaryResponse response) + throws IOException { + JsonpMapper mapper = client._jsonpMapper(); + + if (!(mapper instanceof BufferingJsonpMapper)) { + throw new IllegalArgumentException("ES|QL object mapping currently only works with JacksonJsonpMapper"); + } + + JsonParser parser = mapper.jsonProvider().createParser(response.content()); + + List columns = EsqlAdapterBase.readHeader(parser, mapper).columns; + + List results = new ArrayList<>(); + JsonParser.Event event; + + while ((event = parser.next()) != JsonParser.Event.END_ARRAY) { + // Start of row + JsonpUtils.expectEvent(parser, JsonParser.Event.START_ARRAY, event); + + results.add(parseRow(columns, parser, mapper)); + + // End of row + JsonpUtils.expectNextEvent(parser, JsonParser.Event.END_ARRAY); + } + + EsqlAdapterBase.readFooter(parser); + + return results; + } + + private T parseRow(List columns, JsonParser parser, JsonpMapper mapper) { + // FIXME: add a second implementation not requiring a buffering parser + BufferingJsonGenerator buffer = ((BufferingJsonpMapper) mapper).createBufferingGenerator(); + + buffer.writeStartObject(); + for (EsqlMetadata.EsqlColumn column : columns) { + buffer.writeKey(column.name()); + JsonpUtils.copy(parser, buffer); + } + buffer.writeEnd(); + + return mapper.deserialize(buffer.getParser(), type); + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/Cursor.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/Cursor.java new file mode 100644 index 000000000..53660fe3b --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/Cursor.java @@ -0,0 +1,48 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.sql.SQLException; +import java.util.List; + +interface Cursor { + + List columns(); + + default int columnSize() { + return columns().size(); + } + + boolean next() throws SQLException; + + Object column(int column); + + /** + * Number of rows that this cursor has pulled back from the + * server in the current batch. + */ + int batchSize(); + + void close() throws SQLException; + + List warnings(); + + void clearWarnings(); +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/EsType.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/EsType.java new file mode 100644 index 000000000..0d356d05b --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/EsType.java @@ -0,0 +1,86 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.sql.SQLType; +import java.sql.Types; + +enum EsType implements SQLType { + + NULL(Types.NULL), + UNSUPPORTED(Types.OTHER), + BOOLEAN(Types.BOOLEAN), + BYTE(Types.TINYINT), + SHORT(Types.SMALLINT), + INTEGER(Types.INTEGER), + LONG(Types.BIGINT), + DOUBLE(Types.DOUBLE), + FLOAT(Types.REAL), + HALF_FLOAT(Types.FLOAT), + SCALED_FLOAT(Types.FLOAT), + KEYWORD(Types.VARCHAR), + TEXT(Types.VARCHAR), + OBJECT(Types.STRUCT), + NESTED(Types.STRUCT), + BINARY(Types.VARBINARY), + DATE(Types.DATE), + TIME(Types.TIME), + DATETIME(Types.TIMESTAMP), + IP(Types.VARCHAR), + INTERVAL_YEAR(ExtraTypes.INTERVAL_YEAR), + INTERVAL_MONTH(ExtraTypes.INTERVAL_MONTH), + INTERVAL_YEAR_TO_MONTH(ExtraTypes.INTERVAL_YEAR_MONTH), + INTERVAL_DAY(ExtraTypes.INTERVAL_DAY), + INTERVAL_HOUR(ExtraTypes.INTERVAL_HOUR), + INTERVAL_MINUTE(ExtraTypes.INTERVAL_MINUTE), + INTERVAL_SECOND(ExtraTypes.INTERVAL_SECOND), + INTERVAL_DAY_TO_HOUR(ExtraTypes.INTERVAL_DAY_HOUR), + INTERVAL_DAY_TO_MINUTE(ExtraTypes.INTERVAL_DAY_MINUTE), + INTERVAL_DAY_TO_SECOND(ExtraTypes.INTERVAL_DAY_SECOND), + INTERVAL_HOUR_TO_MINUTE(ExtraTypes.INTERVAL_HOUR_MINUTE), + INTERVAL_HOUR_TO_SECOND(ExtraTypes.INTERVAL_HOUR_SECOND), + INTERVAL_MINUTE_TO_SECOND(ExtraTypes.INTERVAL_MINUTE_SECOND), + GEO_POINT(ExtraTypes.GEOMETRY), + GEO_SHAPE(ExtraTypes.GEOMETRY), + SHAPE(ExtraTypes.GEOMETRY), + UNSIGNED_LONG(Types.NUMERIC), + VERSION(Types.VARCHAR); + + private final Integer type; + + EsType(int type) { + this.type = Integer.valueOf(type); + } + + @Override + public String getName() { + return name(); + } + + @Override + public String getVendor() { + return "co.elastic.clients"; + } + + @Override + public Integer getVendorTypeNumber() { + return type; + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/ExtraTypes.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/ExtraTypes.java new file mode 100644 index 000000000..69d875c1e --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/ExtraTypes.java @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.sql.JDBCType; +import java.sql.Types; + +/** + * Provides ODBC-based codes for the missing SQL data types from {@link Types}/{@link JDBCType}. + */ +final class ExtraTypes { + + private ExtraTypes() {} + + static final int INTERVAL_YEAR = 101; + static final int INTERVAL_MONTH = 102; + static final int INTERVAL_DAY = 103; + static final int INTERVAL_HOUR = 104; + static final int INTERVAL_MINUTE = 105; + static final int INTERVAL_SECOND = 106; + static final int INTERVAL_YEAR_MONTH = 107; + static final int INTERVAL_DAY_HOUR = 108; + static final int INTERVAL_DAY_MINUTE = 109; + static final int INTERVAL_DAY_SECOND = 110; + static final int INTERVAL_HOUR_MINUTE = 111; + static final int INTERVAL_HOUR_SECOND = 112; + static final int INTERVAL_MINUTE_SECOND = 113; + static final int GEOMETRY = 114; + +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcColumnInfo.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcColumnInfo.java new file mode 100644 index 000000000..6043429bc --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcColumnInfo.java @@ -0,0 +1,111 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.sql.SQLException; +import java.util.Objects; + +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.StringUtils.EMPTY; + +class JdbcColumnInfo { + public final String catalog; + public final String schema; + public final String table; + public final String label; + public final String name; + public final int displaySize; + public final EsType type; + + JdbcColumnInfo(String name, String type) throws SQLException { + this(name, TypeUtils.of(type), "", "", "", "", 0); + } + + JdbcColumnInfo(String name, EsType type, String table, String catalog, String schema, String label, int displaySize) { + if (name == null) { + throw new IllegalArgumentException("[name] must not be null"); + } + if (type == null) { + throw new IllegalArgumentException("[type] must not be null"); + } + if (table == null) { + throw new IllegalArgumentException("[table] must not be null"); + } + if (catalog == null) { + throw new IllegalArgumentException("[catalog] must not be null"); + } + if (schema == null) { + throw new IllegalArgumentException("[schema] must not be null"); + } + if (label == null) { + throw new IllegalArgumentException("[label] must not be null"); + } + this.name = name; + this.type = type; + this.table = table; + this.catalog = catalog; + this.schema = schema; + this.label = label; + this.displaySize = displaySize; + } + + int displaySize() { + // 0 - means unknown + return displaySize; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + if (false == EMPTY.equals(table)) { + b.append(table).append('.'); + } + b.append(name).append("').toString(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || obj.getClass() != getClass()) { + return false; + } + JdbcColumnInfo other = (JdbcColumnInfo) obj; + return name.equals(other.name) + && type.equals(other.type) + && table.equals(other.table) + && catalog.equals(other.catalog) + && schema.equals(other.schema) + && label.equals(other.label) + && displaySize == other.displaySize; + } + + @Override + public int hashCode() { + return Objects.hash(name, type, table, catalog, schema, label, displaySize); + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcDateUtils.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcDateUtils.java new file mode 100644 index 000000000..47297d6a0 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcDateUtils.java @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.sql.Date; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.function.Function; + +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.StringUtils.ISO_DATETIME_WITH_NANOS; +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.StringUtils.ISO_TIME_WITH_NANOS; + +/** + * JDBC specific datetime specific utility methods. Because of lack of visibility, this class borrows code + * from {@code org.elasticsearch.xpack.sql.util.DateUtils} and {@code org.elasticsearch.xpack.sql.proto.StringUtils}. + */ +class JdbcDateUtils { + + private JdbcDateUtils() {} + + // In Java 8 LocalDate.EPOCH is not available, introduced with later Java versions + private static final LocalDate EPOCH = LocalDate.of(1970, 1, 1); + + static ZonedDateTime asZonedDateTime(String date) { + return ISO_DATETIME_WITH_NANOS.parse(date, ZonedDateTime::from); + } + + static long dateTimeAsMillisSinceEpoch(String date) { + return asZonedDateTime(date).toInstant().toEpochMilli(); + } + + static long timeAsMillisSinceEpoch(String date) { + return ISO_TIME_WITH_NANOS.parse(date, OffsetTime::from).atDate(EPOCH).toInstant().toEpochMilli(); + } + + static Date asDate(String date) { + ZonedDateTime zdt = asZonedDateTime(date); + return new Date(zdt.toLocalDate().atStartOfDay(zdt.getZone()).toInstant().toEpochMilli()); + } + + static Time asTime(String date) { + ZonedDateTime zdt = asZonedDateTime(date); + return new Time(zdt.toLocalTime().atDate(EPOCH).atZone(zdt.getZone()).toInstant().toEpochMilli()); + } + + static Time timeAsTime(String date) { + OffsetTime ot = ISO_TIME_WITH_NANOS.parse(date, OffsetTime::from); + return new Time(ot.atDate(EPOCH).toInstant().toEpochMilli()); + } + + static Timestamp asTimestamp(long millisSinceEpoch) { + return new Timestamp(millisSinceEpoch); + } + + static Timestamp asTimestamp(String date) { + ZonedDateTime zdt = asZonedDateTime(date); + Timestamp timestamp = new Timestamp(zdt.toInstant().toEpochMilli()); + timestamp.setNanos(zdt.getNano()); + return timestamp; + } + + static Timestamp timeAsTimestamp(String date) { + return new Timestamp(timeAsMillisSinceEpoch(date)); + } + + /* + * Handles the value received as parameter, as either String (a ZonedDateTime formatted in ISO 8601 standard with millis) - + * date fields being returned formatted like this. Or a Long value, in case of Histograms. + */ + static R asDateTimeField(Object value, Function asDateTimeMethod, Function ctor) { + if (value instanceof String) { + return asDateTimeMethod.apply((String) value); + } else { + return ctor.apply(((Number) value).longValue()); + } + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcResultSet.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcResultSet.java new file mode 100644 index 000000000..62836137f --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcResultSet.java @@ -0,0 +1,1274 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.EsType.DATE; +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.EsType.DATETIME; +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.EsType.TIME; +import static java.lang.String.format; + +class JdbcResultSet implements ResultSet, JdbcWrapper { + + // temporary calendar instance (per connection) used for normalizing the date and time + // even though the cfg is already in UTC format, JDBC 3.0 requires java.sql.Time to have its date + // removed (set to Jan 01 1970) and java.sql.Date to have its HH:mm:ss component removed + // instead of dealing with longs, a Calendar object is used instead + private final Calendar defaultCalendar; + + private final Statement statement; + private Cursor cursor; + private final Map nameToIndex = new LinkedHashMap<>(); + + private boolean closed = false; + private boolean wasNull = false; + private boolean wasLast = false; + + private int rowNumber; + + JdbcResultSet(Cursor cursor) { + this.statement = null; + this.cursor = cursor; + // statement can be null so we have to extract the timeZone from the non-nullable cfg + // TODO: should we consider the locale as well? + TimeZone cfgTimeZone = TimeZone.getDefault(); + this.defaultCalendar = Calendar.getInstance(cfgTimeZone, Locale.ROOT); + + List columns = cursor.columns(); + for (int i = 0; i < columns.size(); i++) { + nameToIndex.put(columns.get(i).name, Integer.valueOf(i + 1)); + } + } + + private Object column(int columnIndex) throws SQLException { + checkOpen(); + if (columnIndex < 1 || columnIndex > cursor.columnSize()) { + throw new SQLException("Invalid column index [" + columnIndex + "]"); + } + if (wasLast || rowNumber < 1) { + throw new SQLException("No row available"); + } + Object object = null; + try { + object = cursor.column(columnIndex - 1); + } catch (Exception iae) { + throw new SQLException(iae.getMessage()); + } + wasNull = (object == null); + return object; + } + + private int column(String columnName) throws SQLException { + checkOpen(); + Integer index = nameToIndex.get(columnName); + if (index == null) { + throw new SQLException("Invalid column label [" + columnName + "]"); + } + return index.intValue(); + } + + private EsType columnType(int columnIndex) { + return cursor.columns().get(columnIndex - 1).type; + } + + void checkOpen() throws SQLException { + if (isClosed()) { + throw new SQLException("Closed result set"); + } + } + + @Override + public boolean next() throws SQLException { + checkOpen(); + if (cursor.next()) { + rowNumber++; + return true; + } + wasLast = true; + return false; + } + + @Override + public void close() throws SQLException { + if (closed == false) { + closed = true; +// if (statement != null) { +// statement.resultSetWasClosed(); +// } + cursor.close(); + } + } + + @Override + public boolean wasNull() throws SQLException { + checkOpen(); + return wasNull; + } + + @Override + public String getString(int columnIndex) throws SQLException { + return getObject(columnIndex, String.class); + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + return column(columnIndex) != null ? getObject(columnIndex, Boolean.class) : false; + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + return column(columnIndex) != null ? getObject(columnIndex, Byte.class) : 0; + } + + @Override + public short getShort(int columnIndex) throws SQLException { + return column(columnIndex) != null ? getObject(columnIndex, Short.class) : 0; + } + + @Override + public int getInt(int columnIndex) throws SQLException { + return column(columnIndex) != null ? getObject(columnIndex, Integer.class) : 0; + } + + @Override + public long getLong(int columnIndex) throws SQLException { + return column(columnIndex) != null ? getObject(columnIndex, Long.class) : 0; + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + return column(columnIndex) != null ? getObject(columnIndex, Float.class) : 0; + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + return column(columnIndex) != null ? getObject(columnIndex, Double.class) : 0; + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + try { + return (byte[]) column(columnIndex); + } catch (ClassCastException cce) { + throw new SQLException("unable to convert column " + columnIndex + " to a byte array", cce); + } + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + return asDate(columnIndex); + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + return asTime(columnIndex); + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + return asTimeStamp(columnIndex); + } + + @Override + public String getString(String columnLabel) throws SQLException { + return getString(column(columnLabel)); + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + return getBoolean(column(columnLabel)); + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + return getByte(column(columnLabel)); + } + + @Override + public short getShort(String columnLabel) throws SQLException { + return getShort(column(columnLabel)); + } + + @Override + public int getInt(String columnLabel) throws SQLException { + return getInt(column(columnLabel)); + } + + @Override + public long getLong(String columnLabel) throws SQLException { + return getLong(column(columnLabel)); + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + return getFloat(column(columnLabel)); + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + return getDouble(column(columnLabel)); + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + return getBytes(column(columnLabel)); + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + // TODO: the error message in case the value in the column cannot be converted to a Date refers to a column index + // (for example - "unable to convert column 4 to a long") and not to the column name, which is a bit confusing. + // Should we reconsider this? Maybe by catching the exception here and rethrowing it with the columnLabel instead. + return getDate(column(columnLabel)); + } + + private Long dateTimeAsMillis(int columnIndex) throws SQLException { + Object val = column(columnIndex); + EsType type = columnType(columnIndex); + + if (val == null) { + return null; + } + + try { + // TODO: the B6 appendix of the jdbc spec does mention CHAR, VARCHAR, LONGVARCHAR, DATE, TIMESTAMP as supported + // jdbc types that should be handled by getDate and getTime methods. From all of those we support VARCHAR and + // TIMESTAMP. Should we consider the VARCHAR conversion as a later enhancement? + if (DATETIME == type) { + // the cursor can return an Integer if the date-since-epoch is small enough, XContentParser (Jackson) will + // return the "smallest" data type for numbers when parsing + // TODO: this should probably be handled server side + if (val instanceof String) { + return JdbcDateUtils.asZonedDateTime((String) val).toInstant().toEpochMilli(); + } else { + return ((Number) val).longValue(); + } + } + if (DATE == type) { + return JdbcDateUtils.dateTimeAsMillisSinceEpoch(val.toString()); + } + if (TIME == type) { + return JdbcDateUtils.timeAsMillisSinceEpoch(val.toString()); + } + return (Long) val; + } catch (ClassCastException cce) { + throw new SQLException( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Long", val, type.getName()), + cce + ); + } + } + + private Date asDate(int columnIndex) throws SQLException { + Object val = column(columnIndex); + + if (val == null) { + return null; + } + + EsType type = columnType(columnIndex); + if (type == TIME) { + return new Date(0L); + } + + try { + return JdbcDateUtils.asDate(val.toString()); + } catch (Exception e) { + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Date", val, type.getName()), e); + } + } + + private Time asTime(int columnIndex) throws SQLException { + Object val = column(columnIndex); + + if (val == null) { + return null; + } + + EsType type = columnType(columnIndex); + if (type == DATE) { + return new Time(0L); + } + + try { + if (type == TIME) { + return JdbcDateUtils.timeAsTime(val.toString()); + } + return JdbcDateUtils.asTime(val.toString()); + } catch (Exception e) { + throw new SQLException(format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Time", val, type.getName()), e); + } + } + + private Timestamp asTimeStamp(int columnIndex) throws SQLException { + Object val = column(columnIndex); + + if (val == null) { + return null; + } + + EsType type = columnType(columnIndex); + try { + if (val instanceof Number) { + return JdbcDateUtils.asTimestamp(((Number) val).longValue()); + } + if (type == TIME) { + return JdbcDateUtils.timeAsTimestamp(val.toString()); + } + return JdbcDateUtils.asTimestamp(val.toString()); + } catch (Exception e) { + throw new SQLException( + format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to a Timestamp", val, type.getName()), + e + ); + } + } + + private Calendar safeCalendar(Calendar calendar) { + return calendar == null ? defaultCalendar : calendar; + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + return TypeConverter.convertDate(dateTimeAsMillis(columnIndex), safeCalendar(cal)); + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + return getDate(column(columnLabel), cal); + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + EsType type = columnType(columnIndex); + if (type == DATE) { + return new Time(0L); + } + return TypeConverter.convertTime(dateTimeAsMillis(columnIndex), safeCalendar(cal)); + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + return getTime(column(columnLabel)); + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + Timestamp ts = getTimestamp(columnIndex); + if (ts == null) { + return null; + } + return TypeConverter.convertTimestamp(ts.getTime(), ts.getNanos(), safeCalendar(cal)); + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + return getTimestamp(column(columnLabel)); + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + return getTime(column(columnLabel), cal); + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + return getTimestamp(column(columnLabel), cal); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + return new JdbcResultSetMetaData(this, cursor.columns()); + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + return convert(columnIndex, null); + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + if (type == null) { + throw new SQLException("type is null"); + } + + return convert(columnIndex, type); + } + + private T convert(int columnIndex, Class type) throws SQLException { + Object val = column(columnIndex); + + if (val == null) { + return null; + } + + EsType columnType = columnType(columnIndex); + String typeString = type != null ? type.getSimpleName() : columnType.getName(); + + return TypeConverter.convert(val, columnType, type, typeString); + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + if (map == null || map.isEmpty()) { + return getObject(columnIndex); + } + throw new SQLFeatureNotSupportedException("getObject with non-empty Map not supported"); + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + return getObject(column(columnLabel)); + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + return getObject(column(columnLabel), type); + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + return getObject(column(columnLabel), map); + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + return column(columnLabel); + } + + @Override + public boolean isBeforeFirst() throws SQLException { + return rowNumber == 0; + } + + @Override + public boolean isAfterLast() throws SQLException { + return rowNumber > 0 && wasLast; + } + + @Override + public boolean isFirst() throws SQLException { + return rowNumber == 1; + } + + @Override + public boolean isLast() throws SQLException { + throw new SQLFeatureNotSupportedException("isLast not supported"); + } + + @Override + public int getRow() throws SQLException { + return rowNumber; + } + + @Override + public void setFetchSize(int rows) throws SQLException { + checkOpen(); + if (rows < 0) { + throw new SQLException("Rows is negative"); + } + if (rows != getFetchSize()) { + throw new SQLException("Fetch size cannot be changed"); + } + // ignore fetch size since scrolls cannot be changed in flight + } + + @Override + public int getFetchSize() throws SQLException { + /* + * Instead of returning the fetch size the user requested we make a + * stab at returning the fetch size that we actually used, returning + * the batch size of the current row. This allows us to assert these + * batch sizes in testing and lets us point users to something that + * they can use for debugging. + */ + checkOpen(); + return cursor.batchSize(); + } + + @Override + public Statement getStatement() throws SQLException { + checkOpen(); + return statement; + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + @Deprecated + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + BigDecimal bd = getBigDecimal(columnIndex); + // The API doesn't allow for specifying a rounding behavior, although BigDecimals did have a way of controlling rounding, even + // before the API got deprecated => default to fail if scaling can't return an exactly equal value, since this behavior was + // expected by (old) callers as well. + return bd == null ? null : bd.setScale(scale); + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("AsciiStream not supported"); + } + + @Override + @Deprecated + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("UnicodeStream not supported"); + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("BinaryStream not supported"); + } + + @Override + @Deprecated + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + return getBigDecimal(column(columnLabel), scale); + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("AsciiStream not supported"); + } + + @Override + @Deprecated + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("UnicodeStream not supported"); + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("BinaryStream not supported"); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + checkOpen(); + SQLWarning sqlWarning = null; + for (String warning : cursor.warnings()) { + if (sqlWarning == null) { + sqlWarning = new SQLWarning(warning); + } else { + sqlWarning.setNextWarning(new SQLWarning(warning)); + } + } + return sqlWarning; + } + + @Override + public void clearWarnings() throws SQLException { + checkOpen(); + cursor.clearWarnings(); + } + + @Override + public String getCursorName() throws SQLException { + throw new SQLFeatureNotSupportedException("Cursor name not supported"); + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("CharacterStream not supported"); + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("CharacterStream not supported"); + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + return convert(columnIndex, BigDecimal.class); + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + return getBigDecimal(column(columnLabel)); + } + + @Override + public void beforeFirst() throws SQLException { + throw new SQLException("ResultSet is forward-only"); + } + + @Override + public void afterLast() throws SQLException { + throw new SQLException("ResultSet is forward-only"); + } + + @Override + public boolean first() throws SQLException { + throw new SQLException("ResultSet is forward-only"); + } + + @Override + public boolean last() throws SQLException { + throw new SQLException("ResultSet is forward-only"); + } + + @Override + public boolean absolute(int row) throws SQLException { + throw new SQLException("ResultSet is forward-only"); + } + + @Override + public boolean relative(int rows) throws SQLException { + throw new SQLException("ResultSet is forward-only"); + } + + @Override + public boolean previous() throws SQLException { + throw new SQLException("ResultSet is forward-only"); + } + + @Override + public int getType() throws SQLException { + checkOpen(); + return TYPE_FORWARD_ONLY; + } + + @Override + public int getConcurrency() throws SQLException { + checkOpen(); + return CONCUR_READ_ONLY; + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + checkOpen(); + if (direction != FETCH_FORWARD) { + throw new SQLException("Fetch direction must be FETCH_FORWARD"); + } + } + + @Override + public int getFetchDirection() throws SQLException { + checkOpen(); + return FETCH_FORWARD; + } + + @Override + public boolean rowUpdated() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public boolean rowInserted() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public boolean rowDeleted() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void insertRow() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateRow() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void deleteRow() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void cancelRowUpdates() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void moveToInsertRow() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void refreshRow() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void moveToCurrentRow() throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("Ref not supported"); + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("Blob not supported"); + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("Clob not supported"); + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("Array not supported"); + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("Ref not supported"); + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("Blob not supported"); + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("Clob not supported"); + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("Array not supported"); + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("URL not supported"); + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("URL not supported"); + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("RowId not supported"); + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("RowId not supported"); + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public int getHoldability() throws SQLException { + checkOpen(); + return HOLD_CURSORS_OVER_COMMIT; + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("NClob not supported"); + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("NClob not supported"); + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("SQLXML not supported"); + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("SQLXML not supported"); + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public String getNString(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("NString not supported"); + } + + @Override + public String getNString(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("NString not supported"); + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + throw new SQLFeatureNotSupportedException("NCharacterStream not supported"); + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + throw new SQLFeatureNotSupportedException("NCharacterStream not supported"); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + throw new SQLFeatureNotSupportedException("Writes not supported"); + } + + @Override + public String toString() { + return format( + Locale.ROOT, + "%s:row %d:cursor size %d:%s", + getClass().getSimpleName(), + rowNumber, + cursor.batchSize(), + cursor.columns() + ); + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcResultSetMetaData.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcResultSetMetaData.java new file mode 100644 index 000000000..a404518c5 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcResultSetMetaData.java @@ -0,0 +1,174 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.List; +import java.util.Locale; + +import static java.lang.String.format; +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.StringUtils.EMPTY; + +class JdbcResultSetMetaData implements ResultSetMetaData, JdbcWrapper { + + private final JdbcResultSet rs; + private final List columns; + + JdbcResultSetMetaData(JdbcResultSet rs, List columns) { + this.rs = rs; + this.columns = columns; + } + + @Override + public int getColumnCount() throws SQLException { + checkOpen(); + return columns.size(); + } + + @Override + public boolean isAutoIncrement(int column) throws SQLException { + column(column); + return false; + } + + @Override + public boolean isCaseSensitive(int column) throws SQLException { + column(column); + return true; + } + + @Override + public boolean isSearchable(int column) throws SQLException { + column(column); + return true; + } + + @Override + public boolean isCurrency(int column) throws SQLException { + column(column); + return false; + } + + @Override + public int isNullable(int column) throws SQLException { + column(column); + return columnNullableUnknown; + } + + @Override + public boolean isSigned(int column) throws SQLException { + return TypeUtils.isSigned(column(column).type); + } + + @Override + public int getColumnDisplaySize(int column) throws SQLException { + return column(column).displaySize(); + } + + @Override + public String getColumnLabel(int column) throws SQLException { + JdbcColumnInfo info = column(column); + return EMPTY.equals(info.label) ? info.name : info.label; + } + + @Override + public String getColumnName(int column) throws SQLException { + return column(column).name; + } + + @Override + public String getSchemaName(int column) throws SQLException { + return column(column).schema; + } + + @Override + public int getPrecision(int column) throws SQLException { + column(column); + return 0; + } + + @Override + public int getScale(int column) throws SQLException { + return column(column).displaySize(); + } + + @Override + public String getTableName(int column) throws SQLException { + return column(column).table; + } + + @Override + public String getCatalogName(int column) throws SQLException { + return column(column).catalog; + } + + @Override + public int getColumnType(int column) throws SQLException { + return column(column).type.getVendorTypeNumber(); + } + + @Override + public String getColumnTypeName(int column) throws SQLException { + return column(column).type.getName(); + } + + @Override + public boolean isReadOnly(int column) throws SQLException { + column(column); + return true; + } + + @Override + public boolean isWritable(int column) throws SQLException { + column(column); + return false; + } + + @Override + public boolean isDefinitelyWritable(int column) throws SQLException { + column(column); + return false; + } + + @Override + public String getColumnClassName(int column) throws SQLException { + return TypeUtils.classOf(column(column).type).getName(); + } + + private void checkOpen() throws SQLException { + if (rs != null) { + rs.checkOpen(); + } + } + + private JdbcColumnInfo column(int column) throws SQLException { + checkOpen(); + if (column < 1 || column > columns.size()) { + throw new SQLException("Invalid column index [" + column + "]"); + } + return columns.get(column - 1); + } + + @Override + public String toString() { + return format(Locale.ROOT, "%s(%s)", getClass().getSimpleName(), columns); + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcWrapper.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcWrapper.java new file mode 100644 index 000000000..7fffd10a1 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JdbcWrapper.java @@ -0,0 +1,41 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.sql.SQLException; +import java.sql.Wrapper; + +interface JdbcWrapper extends Wrapper { + + @Override + default boolean isWrapperFor(Class iface) throws SQLException { + return iface != null && iface.isAssignableFrom(getClass()); + } + + @SuppressWarnings("unchecked") + @Override + default T unwrap(Class iface) throws SQLException { + if (isWrapperFor(iface)) { + return (T) this; + } + throw new SQLException(); + } +} + diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JsonpCursor.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JsonpCursor.java new file mode 100644 index 000000000..a3be17c85 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/JsonpCursor.java @@ -0,0 +1,146 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import co.elastic.clients.elasticsearch._helpers.esql.EsqlAdapterBase; +import co.elastic.clients.elasticsearch._helpers.esql.EsqlMetadata; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.json.JsonpUtils; +import co.elastic.clients.json.UnexpectedJsonEventException; +import jakarta.json.stream.JsonParser; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +class JsonpCursor implements Cursor { + + private final JsonParser parser; + private final JsonpMapper mapper; + private final List columns; + private List record = new ArrayList<>(); + + JsonpCursor(EsqlMetadata metadata, JsonParser parser, JsonpMapper mapper) { + this.parser = parser; + this.mapper = mapper; + + this.columns = metadata.columns.stream() + .map(col -> { + try { + return new JdbcColumnInfo(col.name(), col.type()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toList()); + } + + @Override + public List columns() { + return columns; + } + + @Override + public boolean next() throws SQLException { + if (record == null) { + // We're done + return false; + } + + JsonParser.Event event = parser.next(); + if (event != JsonParser.Event.START_ARRAY) { + // End of values + record = null; + JsonpUtils.expectEvent(parser, JsonParser.Event.END_ARRAY, event); + EsqlAdapterBase.readFooter(parser); + return false; + } + + record.clear(); + while ((event = parser.next()) != JsonParser.Event.END_ARRAY) { + record.add(readValue(parser, event)); + } + + return true; + } + + private Object readValue(JsonParser parser, JsonParser.Event event) { + switch (event) { + case VALUE_STRING: + return parser.getString(); + case VALUE_NUMBER: + return parser.isIntegralNumber() ? parser.getLong() : parser.getBigDecimal().doubleValue(); + case VALUE_FALSE: + return Boolean.FALSE; + case VALUE_TRUE: + return Boolean.TRUE; + case VALUE_NULL: + return null; + case START_OBJECT: { + Map map = new HashMap<>(); + while ((event = parser.next()) != JsonParser.Event.END_OBJECT) { + String key = JsonpUtils.expectKeyName(parser, event); + map.put(key, readValue(parser, parser.next())); + } + return map; + } + case START_ARRAY: { + List list = new ArrayList<>(); + while ((event = parser.next()) != JsonParser.Event.END_ARRAY) { + list.add(readValue(parser, event)); + } + return list; + } + default: + throw new UnexpectedJsonEventException(parser, event); + } + } + + @Override + public Object column(int column) { + return record.get(column); + } + + @Override + public int batchSize() { + return 0; + } + + @Override + public void close() throws SQLException { + // Consume the JSON stream + while (next()) { + // Nothing + } + } + + @Override + public List warnings() { + return Collections.emptyList(); + } + + @Override + public void clearWarnings() { + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/ResultSetEsqlAdapter.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/ResultSetEsqlAdapter.java new file mode 100644 index 000000000..fd34334fa --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/ResultSetEsqlAdapter.java @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import co.elastic.clients.ApiClient; +import co.elastic.clients.elasticsearch._helpers.esql.EsqlAdapterBase; +import co.elastic.clients.elasticsearch._helpers.esql.EsqlMetadata; +import co.elastic.clients.elasticsearch.esql.QueryRequest; +import co.elastic.clients.json.JsonpMapper; +import co.elastic.clients.transport.ElasticsearchTransport; +import co.elastic.clients.transport.endpoints.BinaryResponse; +import jakarta.json.stream.JsonParser; + +import java.sql.ResultSet; + +import java.io.IOException; + +public class ResultSetEsqlAdapter extends EsqlAdapterBase { + + public static final ResultSetEsqlAdapter INSTANCE = new ResultSetEsqlAdapter(); + + @Override + public String format() { + return "json"; + } + + @Override + public boolean columnar() { + return false; + } + + @Override + public ResultSet deserialize(ApiClient client, QueryRequest request, BinaryResponse response) + throws IOException { + JsonpMapper mapper = client._jsonpMapper(); + JsonParser parser = mapper.jsonProvider().createParser(response.content()); + EsqlMetadata metadata = EsqlAdapterBase.readHeader(parser, mapper); + return new JdbcResultSet(new JsonpCursor(metadata, parser, mapper)); + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/StringUtils.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/StringUtils.java new file mode 100644 index 000000000..953f693a3 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/StringUtils.java @@ -0,0 +1,169 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.sql.Timestamp; +import java.time.Duration; +import java.time.OffsetTime; +import java.time.Period; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MILLI_OF_SECOND; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.NANO_OF_SECOND; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; + +final class StringUtils { + + public static final String EMPTY = ""; + + public static final DateTimeFormatter ISO_DATETIME_WITH_NANOS = new DateTimeFormatterBuilder().parseCaseInsensitive() + .append(ISO_LOCAL_DATE) + .appendLiteral('T') + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .appendFraction(NANO_OF_SECOND, 3, 9, true) + .appendOffsetId() + .toFormatter(Locale.ROOT); + + public static final DateTimeFormatter ISO_DATETIME_WITH_MILLIS = new DateTimeFormatterBuilder().parseCaseInsensitive() + .append(ISO_LOCAL_DATE) + .appendLiteral('T') + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .appendFraction(MILLI_OF_SECOND, 3, 3, true) + .appendOffsetId() + .toFormatter(Locale.ROOT); + + public static final DateTimeFormatter ISO_TIME_WITH_NANOS = new DateTimeFormatterBuilder().parseCaseInsensitive() + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .appendFraction(NANO_OF_SECOND, 3, 9, true) + .appendOffsetId() + .toFormatter(Locale.ROOT); + + public static final DateTimeFormatter ISO_TIME_WITH_MILLIS = new DateTimeFormatterBuilder().parseCaseInsensitive() + .appendValue(HOUR_OF_DAY, 2) + .appendLiteral(':') + .appendValue(MINUTE_OF_HOUR, 2) + .appendLiteral(':') + .appendValue(SECOND_OF_MINUTE, 2) + .appendFraction(MILLI_OF_SECOND, 3, 3, true) + .appendOffsetId() + .toFormatter(Locale.ROOT); + + private static final int SECONDS_PER_MINUTE = 60; + private static final int SECONDS_PER_HOUR = SECONDS_PER_MINUTE * 60; + private static final int SECONDS_PER_DAY = SECONDS_PER_HOUR * 24; + + private StringUtils() {} + + public static String toString(Object value) { + if (value == null) { + return "null"; + } + + if (value instanceof ZonedDateTime) { + return ((ZonedDateTime) value).format(ISO_DATETIME_WITH_NANOS); + } + if (value instanceof OffsetTime) { + return ((OffsetTime) value).format(ISO_TIME_WITH_NANOS); + } + if (value instanceof Timestamp) { + Timestamp ts = (Timestamp) value; + return ts.toInstant().toString(); + } + + // handle intervals + // YEAR/MONTH/YEAR TO MONTH -> YEAR TO MONTH + if (value instanceof Period) { + // +yyy-mm - 7 chars + StringBuilder sb = new StringBuilder(7); + Period p = (Period) value; + if (p.isNegative()) { + sb.append("-"); + p = p.negated(); + } else { + sb.append("+"); + } + sb.append(p.getYears()); + sb.append("-"); + sb.append(p.getMonths()); + return sb.toString(); + } + + // DAY/HOUR/MINUTE/SECOND (and variations) -> DAY_TO_SECOND + if (value instanceof Duration) { + // +ddd hh:mm:ss.mmmmmmmmm - 23 chars + StringBuilder sb = new StringBuilder(23); + Duration d = (Duration) value; + if (d.isNegative()) { + sb.append("-"); + d = d.negated(); + } else { + sb.append("+"); + } + + long durationInSec = d.getSeconds(); + + sb.append(durationInSec / SECONDS_PER_DAY); + sb.append(" "); + durationInSec = durationInSec % SECONDS_PER_DAY; + sb.append(indent(durationInSec / SECONDS_PER_HOUR)); + sb.append(":"); + durationInSec = durationInSec % SECONDS_PER_HOUR; + sb.append(indent(durationInSec / SECONDS_PER_MINUTE)); + sb.append(":"); + durationInSec = durationInSec % SECONDS_PER_MINUTE; + sb.append(indent(durationInSec)); + long millis = TimeUnit.NANOSECONDS.toMillis(d.getNano()); + if (millis > 0) { + sb.append("."); + while (millis % 10 == 0) { + millis /= 10; + } + sb.append(millis); + } + return sb.toString(); + } + + return Objects.toString(value); + } + + private static String indent(long timeUnit) { + return timeUnit < 10 ? "0" + timeUnit : Long.toString(timeUnit); + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/TypeConverter.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/TypeConverter.java new file mode 100644 index 000000000..2977e98c9 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/TypeConverter.java @@ -0,0 +1,681 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Date; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.function.Function; + +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.JdbcDateUtils.asDateTimeField; +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.JdbcDateUtils.timeAsTime; +import static java.lang.String.format; + +import static java.util.Calendar.DAY_OF_MONTH; +import static java.util.Calendar.ERA; +import static java.util.Calendar.HOUR_OF_DAY; +import static java.util.Calendar.MILLISECOND; +import static java.util.Calendar.MINUTE; +import static java.util.Calendar.MONTH; +import static java.util.Calendar.SECOND; +import static java.util.Calendar.YEAR; +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.EsType.DATE; +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.EsType.DATETIME; +import static co.elastic.clients.elasticsearch._helpers.esql.jdbc.EsType.TIME; + +/** + * Conversion utilities for conversion of JDBC types to Java type and back + *

+ * Only the following JDBC types are supported as part of Elasticsearch response and parameters. + * See org.elasticsearch.xpack.sql.type.DataType for details. + *

+ * NULL, BOOLEAN, TINYINT, SMALLINT, INTEGER, BIGINT, DOUBLE, REAL, FLOAT, VARCHAR, VARBINARY and TIMESTAMP + */ +final class TypeConverter { + + private TypeConverter() {} + + /** + * Converts millisecond after epoc to date + */ + static Date convertDate(Long millis, Calendar cal) { + return dateTimeConvert(millis, cal, c -> { + c.set(HOUR_OF_DAY, 0); + c.set(MINUTE, 0); + c.set(SECOND, 0); + c.set(MILLISECOND, 0); + return new Date(c.getTimeInMillis()); + }); + } + + /** + * Converts millisecond after epoc to time + */ + static Time convertTime(Long millis, Calendar cal) { + return dateTimeConvert(millis, cal, c -> { + c.set(ERA, GregorianCalendar.AD); + c.set(YEAR, 1970); + c.set(MONTH, 0); + c.set(DAY_OF_MONTH, 1); + return new Time(c.getTimeInMillis()); + }); + } + + /** + * Converts millisecond after epoch to timestamp + */ + static Timestamp convertTimestamp(Long millis, int nanos, Calendar cal) { + Timestamp ts = dateTimeConvert(millis, cal, c -> new Timestamp(c.getTimeInMillis())); + if (ts != null) { + ts.setNanos(nanos); + } + return ts; + } + + private static T dateTimeConvert(Long millis, Calendar c, Function creator) { + if (millis == null) { + return null; + } + long initial = c.getTimeInMillis(); + try { + c.setTimeInMillis(millis); + return creator.apply(c); + } finally { + c.setTimeInMillis(initial); + } + } + + /** + * Converts object val from columnType to type + */ + @SuppressWarnings("unchecked") + static T convert(Object val, EsType columnType, Class type, String typeString) throws SQLException { + if (type == null) { + return (T) convert(val, columnType, typeString); + } + + // if the value type is the same as the target, no conversion is needed + // make sure though to check the internal type against the desired one + // since otherwise the internal object format can leak out + // (for example dates when longs are requested or intervals for strings) + if (type.isInstance(val) && TypeUtils.classOf(columnType) == type) { + try { + return type.cast(val); + } catch (ClassCastException cce) { + failConversion(val, columnType, typeString, type, cce); + } + } + + if (type == String.class) { + return (T) asString(convert(val, columnType, typeString)); + } + if (type == Boolean.class) { + return (T) asBoolean(val, columnType, typeString); + } + if (type == Byte.class) { + return (T) asByte(val, columnType, typeString); + } + if (type == Short.class) { + return (T) asShort(val, columnType, typeString); + } + if (type == Integer.class) { + return (T) asInteger(val, columnType, typeString); + } + if (type == Long.class) { + return (T) asLong(val, columnType, typeString); + } + if (type == BigInteger.class) { + return (T) asBigInteger(val, columnType, typeString); + } + if (type == Float.class) { + return (T) asFloat(val, columnType, typeString); + } + if (type == Double.class) { + return (T) asDouble(val, columnType, typeString); + } + if (type == Date.class) { + return (T) asDate(val, columnType, typeString); + } + if (type == Time.class) { + return (T) asTime(val, columnType, typeString); + } + if (type == Timestamp.class) { + return (T) asTimestamp(val, columnType, typeString); + } + if (type == byte[].class) { + return (T) asByteArray(val, columnType, typeString); + } + if (type == BigDecimal.class) { + return (T) asBigDecimal(val, columnType, typeString); + } + // + // JDK 8 types + // + if (type == LocalDate.class) { + return (T) asLocalDate(val, columnType, typeString); + } + if (type == LocalTime.class) { + return (T) asLocalTime(val, columnType, typeString); + } + if (type == LocalDateTime.class) { + return (T) asLocalDateTime(val, columnType, typeString); + } + if (type == OffsetTime.class) { + return (T) asOffsetTime(val, columnType, typeString); + } + if (type == OffsetDateTime.class) { + return (T) asOffsetDateTime(val, columnType, typeString); + } + + return failConversion(val, columnType, typeString, type); + } + + /** + * Converts the object from JSON representation to the specified JDBCType + */ + static Object convert(Object v, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case NULL: + return null; + case BOOLEAN: + case TEXT: + case KEYWORD: + return v; // These types are already represented correctly in JSON + case BYTE: + return ((Number) v).byteValue(); // Parser might return it as integer or long - need to update to the correct type + case SHORT: + return ((Number) v).shortValue(); // Parser might return it as integer or long - need to update to the correct type + case INTEGER: + return ((Number) v).intValue(); + case LONG: + return ((Number) v).longValue(); + case UNSIGNED_LONG: + return asBigInteger(v, columnType, typeString); + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return doubleValue(v); // Double might be represented as string for infinity and NaN values + case FLOAT: + return floatValue(v); // Float might be represented as string for infinity and NaN values + case DATE: + return asDateTimeField(v, JdbcDateUtils::asDate, Date::new); + case TIME: + return timeAsTime(v.toString()); + case DATETIME: + return asDateTimeField(v, JdbcDateUtils::asTimestamp, Timestamp::new); + case INTERVAL_YEAR: + case INTERVAL_MONTH: + case INTERVAL_YEAR_TO_MONTH: + return Period.parse(v.toString()); + case INTERVAL_DAY: + case INTERVAL_HOUR: + case INTERVAL_MINUTE: + case INTERVAL_SECOND: + case INTERVAL_DAY_TO_HOUR: + case INTERVAL_DAY_TO_MINUTE: + case INTERVAL_DAY_TO_SECOND: + case INTERVAL_HOUR_TO_MINUTE: + case INTERVAL_HOUR_TO_SECOND: + case INTERVAL_MINUTE_TO_SECOND: + return Duration.parse(v.toString()); + case GEO_POINT: + case GEO_SHAPE: + case SHAPE: + case IP: + case VERSION: + return v.toString(); + default: + throw new SQLException("Unexpected column type [" + typeString + "]"); + + } + } + + private static Double doubleValue(Object v) { + if (v instanceof String) { + switch ((String) v) { + case "NaN": + return Double.NaN; + case "Infinity": + return Double.POSITIVE_INFINITY; + case "-Infinity": + return Double.NEGATIVE_INFINITY; + default: + return Double.parseDouble((String) v); + } + } + return ((Number) v).doubleValue(); + } + + private static Float floatValue(Object v) { + if (v instanceof String) { + switch ((String) v) { + case "NaN": + return Float.NaN; + case "Infinity": + return Float.POSITIVE_INFINITY; + case "-Infinity": + return Float.NEGATIVE_INFINITY; + default: + return Float.parseFloat((String) v); + } + } + return ((Number) v).floatValue(); + } + + private static String asString(Object nativeValue) { + return nativeValue == null ? null : StringUtils.toString(nativeValue); + } + + private static T failConversion(Object value, EsType columnType, String typeString, Class target) throws SQLException { + return failConversion(value, columnType, typeString, target, null); + } + + private static T failConversion(Object value, EsType columnType, String typeString, Class target, Exception e) + throws SQLException { + String message = format(Locale.ROOT, "Unable to convert value [%.128s] of type [%s] to [%s]", value, columnType, typeString); + throw e != null ? new SQLException(message, e) : new SQLException(message); + } + + private static Boolean asBoolean(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + case BYTE: + case SHORT: + case INTEGER: + case LONG: + case UNSIGNED_LONG: + case FLOAT: + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return Boolean.valueOf(((Number) val).doubleValue() != 0); + case KEYWORD: + case TEXT: + return Boolean.valueOf((String) val); + default: + return failConversion(val, columnType, typeString, Boolean.class); + } + } + + private static Byte asByte(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + return Byte.valueOf(((Boolean) val).booleanValue() ? (byte) 1 : (byte) 0); + case BYTE: + case SHORT: + case INTEGER: + case LONG: + return safeToByte(((Number) val).longValue()); + case UNSIGNED_LONG: + return safeToByte(asBigInteger(val, columnType, typeString)); + case FLOAT: + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return safeToByte(safeToLong(((Number) val).doubleValue())); + case KEYWORD: + case TEXT: + try { + return Byte.valueOf((String) val); + } catch (NumberFormatException e) { + return failConversion(val, columnType, typeString, Byte.class, e); + } + default: + } + + return failConversion(val, columnType, typeString, Byte.class); + } + + private static Short asShort(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + return Short.valueOf(((Boolean) val).booleanValue() ? (short) 1 : (short) 0); + case BYTE: + case SHORT: + case INTEGER: + case LONG: + return safeToShort(((Number) val).longValue()); + case UNSIGNED_LONG: + return safeToShort(asBigInteger(val, columnType, typeString)); + case FLOAT: + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return safeToShort(safeToLong(((Number) val).doubleValue())); + case KEYWORD: + case TEXT: + try { + return Short.valueOf((String) val); + } catch (NumberFormatException e) { + return failConversion(val, columnType, typeString, Short.class, e); + } + default: + } + return failConversion(val, columnType, typeString, Short.class); + } + + private static Integer asInteger(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + return Integer.valueOf(((Boolean) val).booleanValue() ? 1 : 0); + case BYTE: + case SHORT: + case INTEGER: + case LONG: + return safeToInt(((Number) val).longValue()); + case UNSIGNED_LONG: + return safeToInt(asBigInteger(val, columnType, typeString)); + case FLOAT: + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return safeToInt(safeToLong(((Number) val).doubleValue())); + case KEYWORD: + case TEXT: + try { + return Integer.valueOf((String) val); + } catch (NumberFormatException e) { + return failConversion(val, columnType, typeString, Integer.class, e); + } + default: + } + return failConversion(val, columnType, typeString, Integer.class); + } + + private static Long asLong(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + return Long.valueOf(((Boolean) val).booleanValue() ? 1 : 0); + case BYTE: + case SHORT: + case INTEGER: + case LONG: + return Long.valueOf(((Number) val).longValue()); + case UNSIGNED_LONG: + return safeToLong(asBigInteger(val, columnType, typeString)); + case FLOAT: + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return safeToLong(((Number) val).doubleValue()); + // TODO: should we support conversion to TIMESTAMP? + // The spec says that getLong() should support the following types conversions: + // TINYINT, SMALLINT, INTEGER, BIGINT, REAL, FLOAT, DOUBLE, DECIMAL, NUMERIC, BIT, BOOLEAN, CHAR, VARCHAR, LONGVARCHAR + // case TIMESTAMP: + // return ((Number) val).longValue(); + case KEYWORD: + case TEXT: + try { + return Long.valueOf((String) val); + } catch (NumberFormatException e) { + return failConversion(val, columnType, typeString, Long.class, e); + } + default: + } + + return failConversion(val, columnType, typeString, Long.class); + } + + private static Float asFloat(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + return Float.valueOf(((Boolean) val).booleanValue() ? 1 : 0); + case BYTE: + case SHORT: + case INTEGER: + case LONG: + return Float.valueOf(((Number) val).longValue()); + case UNSIGNED_LONG: + return asBigInteger(val, columnType, typeString).floatValue(); + case FLOAT: + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return Float.valueOf(((Number) val).floatValue()); + case KEYWORD: + case TEXT: + try { + return Float.valueOf((String) val); + } catch (NumberFormatException e) { + return failConversion(val, columnType, typeString, Float.class, e); + } + default: + } + return failConversion(val, columnType, typeString, Float.class); + } + + private static Double asDouble(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + return Double.valueOf(((Boolean) val).booleanValue() ? 1 : 0); + case BYTE: + case SHORT: + case INTEGER: + case LONG: + return Double.valueOf(((Number) val).longValue()); + case UNSIGNED_LONG: + return asBigInteger(val, columnType, typeString).doubleValue(); + case FLOAT: + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return Double.valueOf(((Number) val).doubleValue()); + case KEYWORD: + case TEXT: + try { + return Double.valueOf((String) val); + } catch (NumberFormatException e) { + return failConversion(val, columnType, typeString, Double.class, e); + } + default: + } + return failConversion(val, columnType, typeString, Double.class); + } + + private static Date asDate(Object val, EsType columnType, String typeString) throws SQLException { + if (columnType == DATETIME || columnType == DATE) { + return asDateTimeField(val, JdbcDateUtils::asDate, Date::new); + } + if (columnType == TIME) { + return new Date(0L); + } + return failConversion(val, columnType, typeString, Date.class); + } + + private static Time asTime(Object val, EsType columnType, String typeString) throws SQLException { + if (columnType == DATETIME) { + return asDateTimeField(val, JdbcDateUtils::asTime, Time::new); + } + if (columnType == TIME) { + return asDateTimeField(val, JdbcDateUtils::timeAsTime, Time::new); + } + if (columnType == DATE) { + return new Time(0L); + } + return failConversion(val, columnType, typeString, Time.class); + } + + private static Timestamp asTimestamp(Object val, EsType columnType, String typeString) throws SQLException { + if (columnType == DATETIME || columnType == DATE) { + return asDateTimeField(val, JdbcDateUtils::asTimestamp, Timestamp::new); + } + if (columnType == TIME) { + return asDateTimeField(val, JdbcDateUtils::timeAsTimestamp, Timestamp::new); + } + return failConversion(val, columnType, typeString, Timestamp.class); + } + + private static byte[] asByteArray(Object val, EsType columnType, String typeString) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + private static BigInteger asBigInteger(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + return ((Boolean) val).booleanValue() ? BigInteger.ONE : BigInteger.ZERO; + case BYTE: + case SHORT: + case INTEGER: + case LONG: + return BigInteger.valueOf(((Number) val).longValue()); + case FLOAT: + case HALF_FLOAT: + case SCALED_FLOAT: + case DOUBLE: + return BigDecimal.valueOf(((Number) val).doubleValue()).toBigInteger(); + // Aggs can return floats dressed as UL types (bugUrl="https://github.com/elastic/elasticsearch/issues/65413") + case UNSIGNED_LONG: + case KEYWORD: + case TEXT: + try { + return new BigDecimal(val.toString()).toBigInteger(); + } catch (NumberFormatException e) { + return failConversion(val, columnType, typeString, BigInteger.class, e); + } + default: + } + return failConversion(val, columnType, typeString, BigInteger.class); + } + + private static BigDecimal asBigDecimal(Object val, EsType columnType, String typeString) throws SQLException { + switch (columnType) { + case BOOLEAN: + return (Boolean) val ? BigDecimal.ONE : BigDecimal.ZERO; + case BYTE: + case SHORT: + case INTEGER: + case LONG: + return BigDecimal.valueOf(((Number) val).longValue()); + case UNSIGNED_LONG: + return new BigDecimal(asBigInteger(val, columnType, typeString)); + case FLOAT: + case HALF_FLOAT: + // floats are passed in as doubles here, so we need to dip into string to keep original float's (reduced) precision. + return new BigDecimal(String.valueOf(((Number) val).floatValue())); + case DOUBLE: + case SCALED_FLOAT: + return BigDecimal.valueOf(((Number) val).doubleValue()); + case KEYWORD: + case TEXT: + try { + return new BigDecimal((String) val); + } catch (NumberFormatException nfe) { + return failConversion(val, columnType, typeString, BigDecimal.class, nfe); + } + // TODO: should we implement numeric - interval types conversions too; ever needed? ODBC does mandate it + // https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/converting-data-from-c-to-sql-data-types + } + return failConversion(val, columnType, typeString, BigDecimal.class); + } + + private static LocalDate asLocalDate(Object val, EsType columnType, String typeString) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + private static LocalTime asLocalTime(Object val, EsType columnType, String typeString) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + private static LocalDateTime asLocalDateTime(Object val, EsType columnType, String typeString) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + private static OffsetTime asOffsetTime(Object val, EsType columnType, String typeString) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + private static OffsetDateTime asOffsetDateTime(Object val, EsType columnType, String typeString) throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + private static byte safeToByte(Number n) throws SQLException { + if (n instanceof BigInteger) { + try { + return ((BigInteger) n).byteValueExact(); + } catch (ArithmeticException ae) { + throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", n)); + } + } + long x = n.longValue(); + if (x > Byte.MAX_VALUE || x < Byte.MIN_VALUE) { + throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", n)); + } + return (byte) x; + } + + private static short safeToShort(Number n) throws SQLException { + if (n instanceof BigInteger) { + try { + return ((BigInteger) n).shortValueExact(); + } catch (ArithmeticException ae) { + throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", n)); + } + } + long x = n.longValue(); + if (x > Short.MAX_VALUE || x < Short.MIN_VALUE) { + throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", n)); + } + return (short) x; + } + + private static int safeToInt(Number n) throws SQLException { + if (n instanceof BigInteger) { + try { + return ((BigInteger) n).intValueExact(); + } catch (ArithmeticException ae) { + throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", n)); + } + } + long x = n.longValue(); + if (x > Integer.MAX_VALUE || x < Integer.MIN_VALUE) { + throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", n)); + } + return (int) x; + } + + private static long safeToLong(Number n) throws SQLException { + if (n instanceof BigInteger) { + try { + return ((BigInteger) n).longValueExact(); + } catch (ArithmeticException ae) { + throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", n)); + } + } + double x = n.doubleValue(); + if (x > Long.MAX_VALUE || x < Long.MIN_VALUE) { + throw new SQLException(format(Locale.ROOT, "Numeric %s out of range", Double.toString(x))); + } + return Math.round(x); + } +} diff --git a/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/TypeUtils.java b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/TypeUtils.java new file mode 100644 index 000000000..d7a728028 --- /dev/null +++ b/java-client/src/main-flavored/java/co/elastic/clients/elasticsearch/_helpers/esql/jdbc/TypeUtils.java @@ -0,0 +1,147 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql.jdbc; + +import java.math.BigInteger; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.Period; +import java.util.Calendar; +import java.util.Collections; +import java.util.EnumSet; +import java.util.GregorianCalendar; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static java.util.Collections.unmodifiableMap; + +final class TypeUtils { + + private TypeUtils() {} + + private static final Map, EsType> CLASS_TO_TYPE; + private static final Map> TYPE_TO_CLASS; + private static final Map ENUM_NAME_TO_TYPE; + + private static final Set SIGNED_TYPE = EnumSet.of( + EsType.BYTE, + EsType.SHORT, + EsType.INTEGER, + EsType.LONG, + EsType.FLOAT, + EsType.HALF_FLOAT, + EsType.SCALED_FLOAT, + EsType.DOUBLE, + EsType.DATETIME + ); + + public static final int LONG_MAX_LENGTH = String.valueOf(Long.MAX_VALUE).length(); // type length value as defined in ES + + static { + // Note: keep in sync with org.elasticsearch.xpack.sql.qa.jdbc.JdbcTestUtils#CLASS_TO_ES_TYPE + Map, EsType> aMap = new LinkedHashMap<>(); + aMap.put(Boolean.class, EsType.BOOLEAN); + aMap.put(Byte.class, EsType.BYTE); + aMap.put(Short.class, EsType.SHORT); + aMap.put(Integer.class, EsType.INTEGER); + aMap.put(Long.class, EsType.LONG); + aMap.put(BigInteger.class, EsType.UNSIGNED_LONG); + aMap.put(Float.class, EsType.FLOAT); + aMap.put(Double.class, EsType.DOUBLE); + aMap.put(String.class, EsType.KEYWORD); + aMap.put(byte[].class, EsType.BINARY); + aMap.put(Timestamp.class, EsType.DATETIME); + + // apart from the mappings in {@code DataType} three more Java classes can be mapped to a {@code JDBCType.TIMESTAMP} + // according to B-4 table from the jdbc4.2 spec + aMap.put(Calendar.class, EsType.DATETIME); + aMap.put(GregorianCalendar.class, EsType.DATETIME); + aMap.put(java.util.Date.class, EsType.DATETIME); + aMap.put(java.sql.Date.class, EsType.DATETIME); + aMap.put(java.sql.Time.class, EsType.TIME); + aMap.put(LocalDateTime.class, EsType.DATETIME); + CLASS_TO_TYPE = Collections.unmodifiableMap(aMap); + + Map> types = new LinkedHashMap<>(); + types.put(EsType.BOOLEAN, Boolean.class); + types.put(EsType.BYTE, Byte.class); + types.put(EsType.SHORT, Short.class); + types.put(EsType.INTEGER, Integer.class); + types.put(EsType.LONG, Long.class); + types.put(EsType.UNSIGNED_LONG, BigInteger.class); + types.put(EsType.DOUBLE, Double.class); + types.put(EsType.FLOAT, Float.class); + types.put(EsType.HALF_FLOAT, Double.class); + types.put(EsType.SCALED_FLOAT, Double.class); + types.put(EsType.KEYWORD, String.class); + types.put(EsType.TEXT, String.class); + types.put(EsType.BINARY, byte[].class); + types.put(EsType.DATETIME, Timestamp.class); + types.put(EsType.IP, String.class); + types.put(EsType.VERSION, String.class); + types.put(EsType.INTERVAL_YEAR, Period.class); + types.put(EsType.INTERVAL_MONTH, Period.class); + types.put(EsType.INTERVAL_YEAR_TO_MONTH, Period.class); + types.put(EsType.INTERVAL_DAY, Duration.class); + types.put(EsType.INTERVAL_HOUR, Duration.class); + types.put(EsType.INTERVAL_MINUTE, Duration.class); + types.put(EsType.INTERVAL_SECOND, Duration.class); + types.put(EsType.INTERVAL_DAY_TO_HOUR, Duration.class); + types.put(EsType.INTERVAL_DAY_TO_MINUTE, Duration.class); + types.put(EsType.INTERVAL_DAY_TO_SECOND, Duration.class); + types.put(EsType.INTERVAL_HOUR_TO_MINUTE, Duration.class); + types.put(EsType.INTERVAL_HOUR_TO_SECOND, Duration.class); + types.put(EsType.INTERVAL_MINUTE_TO_SECOND, Duration.class); + types.put(EsType.GEO_POINT, String.class); + types.put(EsType.GEO_SHAPE, String.class); + types.put(EsType.SHAPE, String.class); + + TYPE_TO_CLASS = unmodifiableMap(types); + + Map strings = new LinkedHashMap<>(); + + for (EsType dataType : EsType.values()) { + strings.put(dataType.getName().toLowerCase(Locale.ROOT), dataType); + } + + ENUM_NAME_TO_TYPE = unmodifiableMap(strings); + } + + static boolean isSigned(EsType type) { + return SIGNED_TYPE.contains(type); + } + + static Class classOf(EsType type) { + return TYPE_TO_CLASS.get(type); + } + + static EsType of(String name) throws SQLException { + EsType dataType = ENUM_NAME_TO_TYPE.get(name); + if (dataType == null) { + throw new SQLFeatureNotSupportedException("Unsupported Data type [" + name + "]"); + } + return dataType; + } +} diff --git a/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java b/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java index aa350d76b..bad5ca106 100644 --- a/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java +++ b/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/ElasticsearchException.java @@ -38,7 +38,8 @@ public class ElasticsearchException extends RuntimeException { @Nullable private final TransportHttpClient.Response httpResponse; - public ElasticsearchException(String endpointId, ErrorResponse response, @Nullable TransportHttpClient.Response httpResponse) { + public ElasticsearchException(String endpointId, ErrorResponse response, + @Nullable TransportHttpClient.Response httpResponse) { super("[" + endpointId + "] failed: [" + response.error().type() + "] " + response.error().reason()); this.response = response; this.endpointId = endpointId; diff --git a/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/FieldValue.java b/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/FieldValue.java index 9267fae3c..289399e10 100644 --- a/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/FieldValue.java +++ b/java-client/src/main/java/co/elastic/clients/elasticsearch/_types/FieldValue.java @@ -33,7 +33,6 @@ import co.elastic.clients.util.ObjectBuilderBase; import co.elastic.clients.util.TaggedUnion; import co.elastic.clients.util.TaggedUnionUtils; -import jakarta.json.Json; import jakarta.json.stream.JsonGenerator; import jakarta.json.stream.JsonParser; diff --git a/java-client/src/main/java/co/elastic/clients/elasticsearch/esql/ElasticsearchEsqlAsyncClient.java b/java-client/src/main/java/co/elastic/clients/elasticsearch/esql/ElasticsearchEsqlAsyncClient.java index 43034ea3e..383d37bce 100644 --- a/java-client/src/main/java/co/elastic/clients/elasticsearch/esql/ElasticsearchEsqlAsyncClient.java +++ b/java-client/src/main/java/co/elastic/clients/elasticsearch/esql/ElasticsearchEsqlAsyncClient.java @@ -20,6 +20,8 @@ package co.elastic.clients.elasticsearch.esql; import co.elastic.clients.ApiClient; +import co.elastic.clients.elasticsearch._helpers.esql.EsqlAdapter; +import co.elastic.clients.elasticsearch._helpers.esql.EsqlHelper; import co.elastic.clients.elasticsearch._types.ErrorResponse; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.Endpoint; @@ -98,4 +100,43 @@ public final CompletableFuture query( return query(fn.apply(new QueryRequest.Builder()).build()); } + /** + * Executes an ES|QL request and adapts its result to a target type. + * + * @param adapter + * the ES|QL response adapter + * @param query + * the ES|QL query + * @param parameters + * values for query parameters, if any + */ + public final CompletableFuture query(EsqlAdapter adapter, String query, Object... parameters) { + return EsqlHelper.queryAsync(this, adapter, query, parameters); + } + + /** + * Executes an ES|QL request and adapts its result to a target type. + * + * @param adapter + * the ES|QL response adapter + * @param request + * the ES|QL request + */ + public final CompletableFuture query(EsqlAdapter adapter, QueryRequest request) { + return EsqlHelper.queryAsync(this, adapter, request); + } + + /** + * Executes an ES|QL request and adapts its result to a target type. + * + * @param adapter + * the ES|QL response adapter + * @param fn + * the ES|QL request builder + */ + public final CompletableFuture query(EsqlAdapter adapter, + Function> fn) { + return EsqlHelper.queryAsync(this, adapter, fn.apply(new QueryRequest.Builder()).build()); + } + } diff --git a/java-client/src/main/java/co/elastic/clients/elasticsearch/esql/ElasticsearchEsqlClient.java b/java-client/src/main/java/co/elastic/clients/elasticsearch/esql/ElasticsearchEsqlClient.java index ef7b531d7..b3d72e53b 100644 --- a/java-client/src/main/java/co/elastic/clients/elasticsearch/esql/ElasticsearchEsqlClient.java +++ b/java-client/src/main/java/co/elastic/clients/elasticsearch/esql/ElasticsearchEsqlClient.java @@ -20,6 +20,8 @@ package co.elastic.clients.elasticsearch.esql; import co.elastic.clients.ApiClient; +import co.elastic.clients.elasticsearch._helpers.esql.EsqlAdapter; +import co.elastic.clients.elasticsearch._helpers.esql.EsqlHelper; import co.elastic.clients.elasticsearch._types.ElasticsearchException; import co.elastic.clients.elasticsearch._types.ErrorResponse; import co.elastic.clients.transport.ElasticsearchTransport; @@ -99,4 +101,44 @@ public final BinaryResponse query(Function T query(EsqlAdapter adapter, String query, Object... parameters) + throws IOException, ElasticsearchException { + return EsqlHelper.query(this, adapter, query, parameters); + } + + /** + * Executes an ES|QL request and adapts its result to a target type. + * + * @param adapter + * the ES|QL response adapter + * @param request + * the ES|QL request + */ + public final T query(EsqlAdapter adapter, QueryRequest request) throws IOException, ElasticsearchException { + return EsqlHelper.query(this, adapter, request); + } + + /** + * Executes an ES|QL request and adapts its result to a target type. + * + * @param adapter + * the ES|QL response adapter + * @param fn + * the ES|QL request builder + */ + public final T query(EsqlAdapter adapter, Function> fn) + throws IOException, ElasticsearchException { + return query(adapter, fn.apply(new QueryRequest.Builder()).build()); + } + } diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterEndToEndTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterEndToEndTest.java new file mode 100644 index 000000000..557d31703 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterEndToEndTest.java @@ -0,0 +1,194 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql; + +import co.elastic.clients.elasticsearch.ElasticsearchAsyncClient; +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.ElasticsearchTestServer; +import co.elastic.clients.elasticsearch._helpers.esql.jdbc.ResultSetEsqlAdapter; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.transport.rest_client.RestClientTransport; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import org.apache.commons.io.IOUtils; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RestClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class EsqlAdapterEndToEndTest extends Assertions { + + static ElasticsearchClient esClient; + + @BeforeAll + public static void setup() throws Exception { + ElasticsearchClient global = ElasticsearchTestServer.global().client(); + RestClient restClient = ((RestClientTransport) global._transport()).restClient(); + esClient = new ElasticsearchClient(new RestClientTransport(restClient, new JacksonJsonpMapper())); + + esClient.indices().delete(d -> d.index("employees").ignoreUnavailable(true)); + + Request request = new Request("POST", "/employees/_bulk?refresh=true"); + + InputStream resourceAsStream = EsqlAdapterTest.class.getResourceAsStream("employees.ndjson"); + byte[] bytes = IOUtils.toByteArray(resourceAsStream); + request.setEntity(new ByteArrayEntity(bytes, ContentType.APPLICATION_JSON)); + + restClient.performRequest(request); + } + + @Test + public void resultSetTest() throws Exception { + + ResultSet rs = esClient.esql().query( + ResultSetEsqlAdapter.INSTANCE, + "FROM employees | WHERE emp_no == ? or emp_no == ? | KEEP emp_no, job_positions, hire_date | SORT emp_no | LIMIT 300", + // Testing parameters. Note that FROM and LIMIT do not accept parameters + "10042", "10002" + ); + + { + assertTrue(rs.next()); + assertEquals("10002", rs.getString("emp_no")); + + // Single valued fields come back as single value even if other rows have multiple values + @SuppressWarnings("unchecked") + String job = (String) rs.getObject("job_positions"); + assertEquals("Senior Team Lead", job); + } + + { + assertTrue(rs.next()); + assertEquals("10042", rs.getString("emp_no")); + + java.sql.Date hireDate = rs.getDate("hire_date"); + assertEquals("1993-03-21", hireDate.toString()); + + Timestamp hireDate1 = rs.getTimestamp("hire_date"); + assertEquals( + "1993-03-21T00:00:00Z[UTC]", + DateTimeFormatter.ISO_DATE_TIME.format(hireDate1.toInstant().atZone(ZoneId.of("UTC"))) + ); + + @SuppressWarnings("unchecked") + List jobs = (List) rs.getObject("job_positions"); + + assertEquals(4, jobs.size()); + assertEquals("Architect", jobs.get(0)); + } + + assertFalse(rs.next()); + } + + @Test + public void objectsTest() throws Exception { + Iterable result = esClient.esql().query( + ObjectsEsqlAdapter.of(EmpData.class), + "FROM employees | WHERE emp_no == ? or emp_no == ? | KEEP emp_no, job_positions, hire_date | SORT emp_no | LIMIT 300", + // Testing parameters. Note that FROM and LIMIT do not accept parameters + "10042", "10002" + ); + + Iterator it = result.iterator(); + + { + EmpData emp = it.next(); + assertEquals("10002", emp.empNo); + List jobPositions = emp.jobPositions; + // In addition to the value, this tests that single strings are correctly deserialized as a list + assertEquals(Arrays.asList("Senior Team Lead"), emp.jobPositions); + } + + { + EmpData emp = it.next(); + assertEquals("10042", emp.empNo); + assertEquals(Arrays.asList("Architect", "Business Analyst", "Internship", "Junior Developer"), emp.jobPositions); + + assertEquals("1993-03-21T00:00:00Z[UTC]", + DateTimeFormatter.ISO_DATE_TIME.format(emp.hireDate.toInstant().atZone(ZoneId.of("UTC"))) + ); + } + + assertFalse(it.hasNext()); + + } + + @Test + public void asyncObjects() throws Exception { + + ElasticsearchAsyncClient asyncClient = new ElasticsearchAsyncClient(esClient._transport(), esClient._transportOptions()); + + + CompletableFuture> future = asyncClient.esql().query( + ObjectsEsqlAdapter.of(EmpData.class), + "FROM employees | WHERE emp_no == ? or emp_no == ? | KEEP emp_no, job_positions, hire_date | SORT emp_no | LIMIT 300", + // Testing parameters. Note that FROM and LIMIT do not accept parameters + "10042", "10002" + ); + + future.thenApply(result -> { + Iterator it = result.iterator(); + + { + EmpData emp = it.next(); + assertEquals("10002", emp.empNo); + List jobPositions = emp.jobPositions; + // In addition to the value, this tests that single strings are correctly deserialized as a list + assertEquals(Arrays.asList("Senior Team Lead"), emp.jobPositions); + } + + { + EmpData emp = it.next(); + assertEquals("10042", emp.empNo); + assertEquals(Arrays.asList("Architect", "Business Analyst", "Internship", "Junior Developer"), emp.jobPositions); + + assertEquals("1993-03-21T00:00:00Z[UTC]", + DateTimeFormatter.ISO_DATE_TIME.format(emp.hireDate.toInstant().atZone(ZoneId.of("UTC"))) + ); + } + + assertFalse(it.hasNext()); + return null; + } + ).get(); + } + + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + @JsonIgnoreProperties(ignoreUnknown = true) + public static class EmpData { + public String empNo; + public java.util.Date hireDate; + public List jobPositions; + } +} diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterTest.java new file mode 100644 index 000000000..2b67ec1d1 --- /dev/null +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/_helpers/esql/EsqlAdapterTest.java @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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 co.elastic.clients.elasticsearch._helpers.esql; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch._helpers.esql.jdbc.ResultSetEsqlAdapter; +import co.elastic.clients.json.jackson.JacksonJsonpMapper; +import co.elastic.clients.testkit.MockHttpClient; +import co.elastic.clients.transport.endpoints.BinaryResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.Types; + +public class EsqlAdapterTest extends Assertions { + + String json = "{\n" + + " \"columns\": [\n" + + "\t{\"name\": \"avg_salary\", \"type\": \"double\"},\n" + + "\t{\"name\": \"lang\", \t\"type\": \"keyword\"}\n" + + " ],\n" + + " \"values\": [\n" + + "\t[43760.0, \"Spanish\"],\n" + + "\t[48644.0, \"French\"],\n" + + "\t[48832.0, \"German\"]\n" + + " ]\n" + + "}\n"; + + ElasticsearchClient esClient = new MockHttpClient() + .add("/_query", "application/json", json) + .client(new JacksonJsonpMapper()); // object deserializer needs a buffering mapper + + public static class Data { + public double avg_salary; + public String lang; + } + + @Test + public void testObjectDeserializer() throws IOException { + + BinaryResponse response = esClient.esql().query(q -> q + .query("FROM foo") + .format("json") + ); + + Iterable data = esClient.esql().query( + new ObjectsEsqlAdapter<>(Data.class), + "FROM employees | STATS avg_salary = AVG(salary) by country" + ); + + for (Data d: data) { + System.out.println(d.lang + " " + d.avg_salary); + } + } + + @Test + public void testResultSetAdapter() throws Exception { + + ResultSet resultSet = esClient.esql().query( + ResultSetEsqlAdapter.INSTANCE, + "FROM employees | STATS avg_salary = AVG(salary) by country" + ); + + assertEquals(2, resultSet.getMetaData().getColumnCount()); + assertEquals(Types.DOUBLE, resultSet.getMetaData().getColumnType(1)); + assertEquals(Types.VARCHAR, resultSet.getMetaData().getColumnType(2)); + + while (resultSet.next()) { + System.out.println(resultSet.getDouble("avg_salary") + " " + resultSet.getString(2)); + } + } +} diff --git a/java-client/src/test/resources/co/elastic/clients/elasticsearch/_helpers/esql/employees.ndjson b/java-client/src/test/resources/co/elastic/clients/elasticsearch/_helpers/esql/employees.ndjson new file mode 100644 index 000000000..f1e9adf7e --- /dev/null +++ b/java-client/src/test/resources/co/elastic/clients/elasticsearch/_helpers/esql/employees.ndjson @@ -0,0 +1,201 @@ +{ "index" : {} } +{"birth_date":["1953-09-02T00:00:00Z"],"emp_no":["10001"],"first_name":["Georgi"],"gender":["M"],"hire_date":["1986-06-26T00:00:00Z"],"languages":["2"],"last_name":["Facello"],"salary":["57305"],"height":["2.03"],"still_hired":["true"],"avg_worked_seconds":["268728049"],"job_positions":["Senior Python Developer","Accountant"],"is_rehired":["false","true"],"salary_change":["1.19"]} +{ "index" : {} } +{"birth_date":["1964-06-02T00:00:00Z"],"emp_no":["10002"],"first_name":["Bezalel"],"gender":["F"],"hire_date":["1985-11-21T00:00:00Z"],"languages":["5"],"last_name":["Simmel"],"salary":["56371"],"height":["2.08"],"still_hired":["true"],"avg_worked_seconds":["328922887"],"job_positions":["Senior Team Lead"],"is_rehired":["false","false"],"salary_change":["-7.23","11.17"]} +{ "index" : {} } +{"birth_date":["1959-12-03T00:00:00Z"],"emp_no":["10003"],"first_name":["Parto"],"gender":["M"],"hire_date":["1986-08-28T00:00:00Z"],"languages":["4"],"last_name":["Bamford"],"salary":["61805"],"height":["1.83"],"still_hired":["false"],"avg_worked_seconds":["200296405"],"salary_change":["14.68","12.82"]} +{ "index" : {} } +{"birth_date":["1954-05-01T00:00:00Z"],"emp_no":["10004"],"first_name":["Chirstian"],"gender":["M"],"hire_date":["1986-12-01T00:00:00Z"],"languages":["5"],"last_name":["Koblick"],"salary":["36174"],"height":["1.78"],"still_hired":["true"],"avg_worked_seconds":["311267831"],"job_positions":["Reporting Analyst","Tech Lead","Head Human Resources","Support Engineer"],"is_rehired":["true"],"salary_change":["3.65","-0.35","1.13","13.48"]} +{ "index" : {} } +{"birth_date":["1955-01-21T00:00:00Z"],"emp_no":["10005"],"first_name":["Kyoichi"],"gender":["M"],"hire_date":["1989-09-12T00:00:00Z"],"languages":["1"],"last_name":["Maliniak"],"salary":["63528"],"height":["2.05"],"still_hired":["true"],"avg_worked_seconds":["244294991"],"is_rehired":["false","false","false","true"],"salary_change":["-2.14","13.07"]} +{ "index" : {} } +{"birth_date":["1953-04-20T00:00:00Z"],"emp_no":["10006"],"first_name":["Anneke"],"gender":["F"],"hire_date":["1989-06-02T00:00:00Z"],"languages":["3"],"last_name":["Preusig"],"salary":["60335"],"height":["1.56"],"still_hired":["false"],"avg_worked_seconds":["372957040"],"job_positions":["Tech Lead","Principal Support Engineer","Senior Team Lead"],"salary_change":["-3.90"]} +{ "index" : {} } +{"birth_date":["1957-05-23T00:00:00Z"],"emp_no":["10007"],"first_name":["Tzvetan"],"gender":["F"],"hire_date":["1989-02-10T00:00:00Z"],"languages":["4"],"last_name":["Zielinski"],"salary":["74572"],"height":["1.70"],"still_hired":["true"],"avg_worked_seconds":["393084805"],"is_rehired":["true","false","true","false"],"salary_change":["-7.06","1.99","0.57"]} +{ "index" : {} } +{"birth_date":["1958-02-19T00:00:00Z"],"emp_no":["10008"],"first_name":["Saniya"],"gender":["M"],"hire_date":["1994-09-15T00:00:00Z"],"languages":["2"],"last_name":["Kalloufi"],"salary":["43906"],"height":["2.10"],"still_hired":["true"],"avg_worked_seconds":["283074758"],"job_positions":["Senior Python Developer","Junior Developer","Purchase Manager","Internship"],"is_rehired":["true","false"],"salary_change":["12.68","3.54","0.75","-2.92"]} +{ "index" : {} } +{"birth_date":["1952-04-19T00:00:00Z"],"emp_no":["10009"],"first_name":["Sumant"],"gender":["F"],"hire_date":["1985-02-18T00:00:00Z"],"languages":["1"],"last_name":["Peac"],"salary":["66174"],"height":["1.85"],"still_hired":["false"],"avg_worked_seconds":["236805489"],"job_positions":["Senior Python Developer","Internship"]} +{ "index" : {} } +{"birth_date":["1963-06-01T00:00:00Z"],"emp_no":["10010"],"first_name":["Duangkaew"],"hire_date":["1989-08-24T00:00:00Z"],"languages":["4"],"last_name":["Piveteau"],"salary":["45797"],"height":["1.70"],"still_hired":["false"],"avg_worked_seconds":["315236372"],"job_positions":["Architect","Reporting Analyst","Tech Lead","Purchase Manager"],"is_rehired":["true","true","false","false"],"salary_change":["5.05","-6.77","4.69","12.15"]} +{ "index" : {} } +{"birth_date":["1953-11-07T00:00:00Z"],"emp_no":["10011"],"first_name":["Mary"],"hire_date":["1990-01-22T00:00:00Z"],"languages":["5"],"last_name":["Sluis"],"salary":["31120"],"height":["1.50"],"still_hired":["true"],"avg_worked_seconds":["239615525"],"job_positions":["Architect","Reporting Analyst","Tech Lead","Senior Team Lead"],"is_rehired":["true","true"],"salary_change":["10.35","-7.82","8.73","3.48"]} +{ "index" : {} } +{"birth_date":["1960-10-04T00:00:00Z"],"emp_no":["10012"],"first_name":["Patricio"],"hire_date":["1992-12-18T00:00:00Z"],"languages":["5"],"last_name":["Bridgland"],"salary":["48942"],"height":["1.97"],"still_hired":["false"],"avg_worked_seconds":["365510850"],"job_positions":["Head Human Resources","Accountant"],"is_rehired":["false","true","true","false"],"salary_change":["0.04"]} +{ "index" : {} } +{"birth_date":["1963-06-07T00:00:00Z"],"emp_no":["10013"],"first_name":["Eberhardt"],"hire_date":["1985-10-20T00:00:00Z"],"languages":["1"],"last_name":["Terkki"],"salary":["48735"],"height":["1.94"],"still_hired":["true"],"avg_worked_seconds":["253864340"],"job_positions":["Reporting Analyst"],"is_rehired":["true","true"]} +{ "index" : {} } +{"birth_date":["1956-02-12T00:00:00Z"],"emp_no":["10014"],"first_name":["Berni"],"hire_date":["1987-03-11T00:00:00Z"],"languages":["5"],"last_name":["Genin"],"salary":["37137"],"height":["1.99"],"still_hired":["false"],"avg_worked_seconds":["225049139"],"job_positions":["Reporting Analyst","Data Scientist","Head Human Resources"],"salary_change":["-1.89","9.07"]} +{ "index" : {} } +{"birth_date":["1959-08-19T00:00:00Z"],"emp_no":["10015"],"first_name":["Guoxiang"],"hire_date":["1987-07-02T00:00:00Z"],"languages":["5"],"last_name":["Nooteboom"],"salary":["25324"],"height":["1.66"],"still_hired":["true"],"avg_worked_seconds":["390266432"],"job_positions":["Principal Support Engineer","Junior Developer","Head Human Resources","Support Engineer"],"is_rehired":["true","false","false","false"],"salary_change":["14.25","12.40"]} +{ "index" : {} } +{"birth_date":["1961-05-02T00:00:00Z"],"emp_no":["10016"],"first_name":["Kazuhito"],"hire_date":["1995-01-27T00:00:00Z"],"languages":["2"],"last_name":["Cappelletti"],"salary":["61358"],"height":["1.54"],"still_hired":["false"],"avg_worked_seconds":["253029411"],"job_positions":["Reporting Analyst","Python Developer","Accountant","Purchase Manager"],"is_rehired":["false","false"],"salary_change":["-5.18","7.69"]} +{ "index" : {} } +{"birth_date":["1958-07-06T00:00:00Z"],"emp_no":["10017"],"first_name":["Cristinel"],"hire_date":["1993-08-03T00:00:00Z"],"languages":["2"],"last_name":["Bouloucos"],"salary":["58715"],"height":["1.74"],"still_hired":["false"],"avg_worked_seconds":["236703986"],"job_positions":["Data Scientist","Head Human Resources","Purchase Manager"],"is_rehired":["true","false","true","true"],"salary_change":["-6.33"]} +{ "index" : {} } +{"birth_date":["1954-06-19T00:00:00Z"],"emp_no":["10018"],"first_name":["Kazuhide"],"hire_date":["1987-04-03T00:00:00Z"],"languages":["2"],"last_name":["Peha"],"salary":["56760"],"height":["1.97"],"still_hired":["false"],"avg_worked_seconds":["309604079"],"job_positions":["Junior Developer"],"is_rehired":["false","false","true","true"],"salary_change":["-1.64","11.51","-5.32"]} +{ "index" : {} } +{"birth_date":["1953-01-23T00:00:00Z"],"emp_no":["10019"],"first_name":["Lillian"],"hire_date":["1999-04-30T00:00:00Z"],"languages":["1"],"last_name":["Haddadi"],"salary":["73717"],"height":["2.06"],"still_hired":["false"],"avg_worked_seconds":["342855721"],"job_positions":["Purchase Manager"],"is_rehired":["false","false"],"salary_change":["-6.84","8.42","-7.26"]} +{ "index" : {} } +{"birth_date":["1952-12-24T00:00:00Z"],"emp_no":["10020"],"first_name":["Mayuko"],"gender":["M"],"hire_date":["1991-01-26T00:00:00Z"],"last_name":["Warwick"],"salary":["40031"],"height":["1.41"],"still_hired":["false"],"avg_worked_seconds":["373309605"],"job_positions":["Tech Lead"],"is_rehired":["true","true","false"],"salary_change":["-5.81"]} +{ "index" : {} } +{"birth_date":["1960-02-20T00:00:00Z"],"emp_no":["10021"],"first_name":["Ramzi"],"gender":["M"],"hire_date":["1988-02-10T00:00:00Z"],"last_name":["Erde"],"salary":["60408"],"height":["1.47"],"still_hired":["false"],"avg_worked_seconds":["287654610"],"job_positions":["Support Engineer"],"is_rehired":["true"]} +{ "index" : {} } +{"birth_date":["1952-07-08T00:00:00Z"],"emp_no":["10022"],"first_name":["Shahaf"],"gender":["M"],"hire_date":["1995-08-22T00:00:00Z"],"last_name":["Famili"],"salary":["48233"],"height":["1.82"],"still_hired":["false"],"avg_worked_seconds":["233521306"],"job_positions":["Reporting Analyst","Data Scientist","Python Developer","Internship"],"is_rehired":["true","false"],"salary_change":["12.09","2.85"]} +{ "index" : {} } +{"birth_date":["1953-09-29T00:00:00Z"],"emp_no":["10023"],"first_name":["Bojan"],"gender":["F"],"hire_date":["1989-12-17T00:00:00Z"],"last_name":["Montemayor"],"salary":["47896"],"height":["1.75"],"still_hired":["true"],"avg_worked_seconds":["330870342"],"job_positions":["Accountant","Support Engineer","Purchase Manager"],"is_rehired":["true","true","false"],"salary_change":["14.63","0.80"]} +{ "index" : {} } +{"birth_date":["1958-09-05T00:00:00Z"],"emp_no":["10024"],"first_name":["Suzette"],"gender":["F"],"hire_date":["1997-05-19T00:00:00Z"],"last_name":["Pettey"],"salary":["64675"],"height":["2.08"],"still_hired":["true"],"avg_worked_seconds":["367717671"],"job_positions":["Junior Developer"],"is_rehired":["true","true","true","true"]} +{ "index" : {} } +{"birth_date":["1958-10-31T00:00:00Z"],"emp_no":["10025"],"first_name":["Prasadram"],"gender":["M"],"hire_date":["1987-08-17T00:00:00Z"],"last_name":["Heyers"],"salary":["47411"],"height":["1.87"],"still_hired":["false"],"avg_worked_seconds":["371270797"],"job_positions":["Accountant"],"is_rehired":["true","false"],"salary_change":["-4.33","-2.90","12.06","-3.46"]} +{ "index" : {} } +{"birth_date":["1953-04-03T00:00:00Z"],"emp_no":["10026"],"first_name":["Yongqiao"],"gender":["M"],"hire_date":["1995-03-20T00:00:00Z"],"last_name":["Berztiss"],"salary":["28336"],"height":["2.10"],"still_hired":["true"],"avg_worked_seconds":["359208133"],"job_positions":["Reporting Analyst"],"is_rehired":["false","true"],"salary_change":["-7.37","10.62","11.20"]} +{ "index" : {} } +{"birth_date":["1962-07-10T00:00:00Z"],"emp_no":["10027"],"first_name":["Divier"],"gender":["F"],"hire_date":["1989-07-07T00:00:00Z"],"last_name":["Reistad"],"salary":["73851"],"height":["1.53"],"still_hired":["false"],"avg_worked_seconds":["374037782"],"job_positions":["Senior Python Developer"],"is_rehired":["false"]} +{ "index" : {} } +{"birth_date":["1963-11-26T00:00:00Z"],"emp_no":["10028"],"first_name":["Domenick"],"gender":["M"],"hire_date":["1991-10-22T00:00:00Z"],"last_name":["Tempesti"],"salary":["39356"],"height":["2.07"],"still_hired":["true"],"avg_worked_seconds":["226435054"],"job_positions":["Tech Lead","Python Developer","Accountant","Internship"],"is_rehired":["true","false","false","true"]} +{ "index" : {} } +{"birth_date":["1956-12-13T00:00:00Z"],"emp_no":["10029"],"first_name":["Otmar"],"gender":["M"],"hire_date":["1985-11-20T00:00:00Z"],"last_name":["Herbst"],"salary":["74999"],"height":["1.99"],"still_hired":["false"],"avg_worked_seconds":["257694181"],"job_positions":["Senior Python Developer","Data Scientist","Principal Support Engineer"],"is_rehired":["true"],"salary_change":["-0.32","-1.90","-8.19"]} +{ "index" : {} } +{"birth_date":["1958-07-14T00:00:00Z"],"emp_no":["10030"],"gender":["M"],"hire_date":["1994-02-17T00:00:00Z"],"languages":["3"],"last_name":["Demeyer"],"salary":["67492"],"height":["1.92"],"still_hired":["false"],"avg_worked_seconds":["394597613"],"job_positions":["Tech Lead","Data Scientist","Senior Team Lead"],"is_rehired":["true","false","false"],"salary_change":["-0.40"]} +{ "index" : {} } +{"birth_date":["1959-01-27T00:00:00Z"],"emp_no":["10031"],"gender":["M"],"hire_date":["1991-09-01T00:00:00Z"],"languages":["4"],"last_name":["Joslin"],"salary":["37716"],"height":["1.68"],"still_hired":["false"],"avg_worked_seconds":["348545109"],"job_positions":["Architect","Senior Python Developer","Purchase Manager","Senior Team Lead"],"is_rehired":["false"]} +{ "index" : {} } +{"birth_date":["1960-08-09T00:00:00Z"],"emp_no":["10032"],"gender":["F"],"hire_date":["1990-06-20T00:00:00Z"],"languages":["3"],"last_name":["Reistad"],"salary":["62233"],"height":["2.10"],"still_hired":["false"],"avg_worked_seconds":["277622619"],"job_positions":["Architect","Senior Python Developer","Junior Developer","Purchase Manager"],"is_rehired":["false","false"],"salary_change":["9.32","-4.92"]} +{ "index" : {} } +{"birth_date":["1956-11-14T00:00:00Z"],"emp_no":["10033"],"gender":["M"],"hire_date":["1987-03-18T00:00:00Z"],"languages":["1"],"last_name":["Merlo"],"salary":["70011"],"height":["1.63"],"still_hired":["false"],"avg_worked_seconds":["208374744"],"is_rehired":["true"]} +{ "index" : {} } +{"birth_date":["1962-12-29T00:00:00Z"],"emp_no":["10034"],"gender":["M"],"hire_date":["1988-09-21T00:00:00Z"],"languages":["1"],"last_name":["Swan"],"salary":["39878"],"height":["1.46"],"still_hired":["false"],"avg_worked_seconds":["214393176"],"job_positions":["Business Analyst","Data Scientist","Python Developer","Accountant"],"is_rehired":["false"],"salary_change":["-8.46"]} +{ "index" : {} } +{"birth_date":["1953-02-08T00:00:00Z"],"emp_no":["10035"],"gender":["M"],"hire_date":["1988-09-05T00:00:00Z"],"languages":["5"],"last_name":["Chappelet"],"salary":["25945"],"height":["1.81"],"still_hired":["false"],"avg_worked_seconds":["203838153"],"job_positions":["Senior Python Developer","Data Scientist"],"is_rehired":["false"],"salary_change":["-2.54","-6.58"]} +{ "index" : {} } +{"birth_date":["1959-08-10T00:00:00Z"],"emp_no":["10036"],"gender":["M"],"hire_date":["1992-01-03T00:00:00Z"],"languages":["4"],"last_name":["Portugali"],"salary":["60781"],"height":["1.61"],"still_hired":["false"],"avg_worked_seconds":["305493131"],"job_positions":["Senior Python Developer"],"is_rehired":["true","false","false"]} +{ "index" : {} } +{"birth_date":["1963-07-22T00:00:00Z"],"emp_no":["10037"],"gender":["M"],"hire_date":["1990-12-05T00:00:00Z"],"languages":["2"],"last_name":["Makrucki"],"salary":["37691"],"height":["2.00"],"still_hired":["true"],"avg_worked_seconds":["359217000"],"job_positions":["Senior Python Developer","Tech Lead","Accountant"],"is_rehired":["false"],"salary_change":["-7.08"]} +{ "index" : {} } +{"birth_date":["1960-07-20T00:00:00Z"],"emp_no":["10038"],"gender":["M"],"hire_date":["1989-09-20T00:00:00Z"],"languages":["4"],"last_name":["Lortz"],"salary":["35222"],"height":["1.53"],"still_hired":["true"],"avg_worked_seconds":["314036411"],"job_positions":["Senior Python Developer","Python Developer","Support Engineer"]} +{ "index" : {} } +{"birth_date":["1959-10-01T00:00:00Z"],"emp_no":["10039"],"gender":["M"],"hire_date":["1988-01-19T00:00:00Z"],"languages":["2"],"last_name":["Brender"],"salary":["36051"],"height":["1.55"],"still_hired":["false"],"avg_worked_seconds":["243221262"],"job_positions":["Business Analyst","Python Developer","Principal Support Engineer"],"is_rehired":["true","true"],"salary_change":["-6.90"]} +{ "index" : {} } +{"emp_no":["10040"],"first_name":["Weiyi"],"gender":["F"],"hire_date":["1993-02-14T00:00:00Z"],"languages":["4"],"last_name":["Meriste"],"salary":["37112"],"height":["1.90"],"still_hired":["false"],"avg_worked_seconds":["244478622"],"job_positions":["Principal Support Engineer"],"is_rehired":["true","false","true","true"],"salary_change":["6.97","14.74","-8.94","1.92"]} +{ "index" : {} } +{"emp_no":["10041"],"first_name":["Uri"],"gender":["F"],"hire_date":["1989-11-12T00:00:00Z"],"languages":["1"],"last_name":["Lenart"],"salary":["56415"],"height":["1.75"],"still_hired":["false"],"avg_worked_seconds":["287789442"],"job_positions":["Data Scientist","Head Human Resources","Internship","Senior Team Lead"],"salary_change":["9.21","0.05","7.29","-2.94"]} +{ "index" : {} } +{"emp_no":["10042"],"first_name":["Magy"],"gender":["F"],"hire_date":["1993-03-21T00:00:00Z"],"languages":["3"],"last_name":["Stamatiou"],"salary":["30404"],"height":["1.44"],"still_hired":["true"],"avg_worked_seconds":["246355863"],"job_positions":["Architect","Business Analyst","Junior Developer","Internship"],"salary_change":["-9.28","9.42"]} +{ "index" : {} } +{"emp_no":["10043"],"first_name":["Yishay"],"gender":["M"],"hire_date":["1990-10-20T00:00:00Z"],"languages":["1"],"last_name":["Tzvieli"],"salary":["34341"],"height":["1.52"],"still_hired":["true"],"avg_worked_seconds":["287222180"],"job_positions":["Data Scientist","Python Developer","Support Engineer"],"is_rehired":["false","true","true"],"salary_change":["-5.17","4.62","7.42"]} +{ "index" : {} } +{"emp_no":["10044"],"first_name":["Mingsen"],"gender":["F"],"hire_date":["1994-05-21T00:00:00Z"],"languages":["1"],"last_name":["Casley"],"salary":["39728"],"height":["2.06"],"still_hired":["false"],"avg_worked_seconds":["387408356"],"job_positions":["Tech Lead","Principal Support Engineer","Accountant","Support Engineer"],"is_rehired":["true","true"],"salary_change":["8.09"]} +{ "index" : {} } +{"emp_no":["10045"],"first_name":["Moss"],"gender":["M"],"hire_date":["1989-09-02T00:00:00Z"],"languages":["3"],"last_name":["Shanbhogue"],"salary":["74970"],"height":["1.70"],"still_hired":["false"],"avg_worked_seconds":["371418933"],"job_positions":["Principal Support Engineer","Junior Developer","Accountant","Purchase Manager"],"is_rehired":["true","false"]} +{ "index" : {} } +{"emp_no":["10046"],"first_name":["Lucien"],"gender":["M"],"hire_date":["1992-06-20T00:00:00Z"],"languages":["4"],"last_name":["Rosenbaum"],"salary":["50064"],"height":["1.52"],"still_hired":["true"],"avg_worked_seconds":["302353405"],"job_positions":["Principal Support Engineer","Junior Developer","Head Human Resources","Internship"],"is_rehired":["true","true","false","true"],"salary_change":["2.39"]} +{ "index" : {} } +{"emp_no":["10047"],"first_name":["Zvonko"],"gender":["M"],"hire_date":["1989-03-31T00:00:00Z"],"languages":["4"],"last_name":["Nyanchama"],"salary":["42716"],"height":["1.52"],"still_hired":["true"],"avg_worked_seconds":["306369346"],"job_positions":["Architect","Data Scientist","Principal Support Engineer","Senior Team Lead"],"is_rehired":["true"],"salary_change":["-6.36","12.12"]} +{ "index" : {} } +{"emp_no":["10048"],"first_name":["Florian"],"gender":["M"],"hire_date":["1985-02-24T00:00:00Z"],"languages":["3"],"last_name":["Syrotiuk"],"salary":["26436"],"height":["2.00"],"still_hired":["false"],"avg_worked_seconds":["248451647"],"job_positions":["Internship"],"is_rehired":["true","true"]} +{ "index" : {} } +{"emp_no":["10049"],"first_name":["Basil"],"gender":["F"],"hire_date":["1992-05-04T00:00:00Z"],"languages":["5"],"last_name":["Tramer"],"salary":["37853"],"height":["1.52"],"still_hired":["true"],"avg_worked_seconds":["320725709"],"job_positions":["Senior Python Developer","Business Analyst"],"salary_change":["-1.05"]} +{ "index" : {} } +{"birth_date":["1958-05-21T00:00:00Z"],"emp_no":["10050"],"first_name":["Yinghua"],"gender":["M"],"hire_date":["1990-12-25T00:00:00Z"],"languages":["2"],"last_name":["Dredge"],"salary":["43026"],"height":["1.96"],"still_hired":["true"],"avg_worked_seconds":["242731798"],"job_positions":["Reporting Analyst","Junior Developer","Accountant","Support Engineer"],"is_rehired":["true"],"salary_change":["8.70","10.94"]} +{ "index" : {} } +{"birth_date":["1953-07-28T00:00:00Z"],"emp_no":["10051"],"first_name":["Hidefumi"],"gender":["M"],"hire_date":["1992-10-15T00:00:00Z"],"languages":["3"],"last_name":["Caine"],"salary":["58121"],"height":["1.89"],"still_hired":["true"],"avg_worked_seconds":["374753122"],"job_positions":["Business Analyst","Accountant","Purchase Manager"]} +{ "index" : {} } +{"birth_date":["1961-02-26T00:00:00Z"],"emp_no":["10052"],"first_name":["Heping"],"gender":["M"],"hire_date":["1988-05-21T00:00:00Z"],"languages":["1"],"last_name":["Nitsch"],"salary":["55360"],"height":["1.79"],"still_hired":["true"],"avg_worked_seconds":["299654717"],"is_rehired":["true","true","false"],"salary_change":["-0.55","-1.89","-4.22","-6.03"]} +{ "index" : {} } +{"birth_date":["1954-09-13T00:00:00Z"],"emp_no":["10053"],"first_name":["Sanjiv"],"gender":["F"],"hire_date":["1986-02-04T00:00:00Z"],"languages":["3"],"last_name":["Zschoche"],"salary":["54462"],"height":["1.58"],"still_hired":["false"],"avg_worked_seconds":["368103911"],"job_positions":["Support Engineer"],"is_rehired":["true","false","true","false"],"salary_change":["-7.67","-3.25"]} +{ "index" : {} } +{"birth_date":["1957-04-04T00:00:00Z"],"emp_no":["10054"],"first_name":["Mayumi"],"gender":["M"],"hire_date":["1995-03-13T00:00:00Z"],"languages":["4"],"last_name":["Schueller"],"salary":["65367"],"height":["1.82"],"still_hired":["false"],"avg_worked_seconds":["297441693"],"job_positions":["Principal Support Engineer"],"is_rehired":["false","false"]} +{ "index" : {} } +{"birth_date":["1956-06-06T00:00:00Z"],"emp_no":["10055"],"first_name":["Georgy"],"gender":["M"],"hire_date":["1992-04-27T00:00:00Z"],"languages":["5"],"last_name":["Dredge"],"salary":["49281"],"height":["2.04"],"still_hired":["false"],"avg_worked_seconds":["283157844"],"job_positions":["Senior Python Developer","Head Human Resources","Internship","Support Engineer"],"is_rehired":["false","false","true"],"salary_change":["7.34","12.99","3.17"]} +{ "index" : {} } +{"birth_date":["1961-09-01T00:00:00Z"],"emp_no":["10056"],"first_name":["Brendon"],"gender":["F"],"hire_date":["1990-02-01T00:00:00Z"],"languages":["2"],"last_name":["Bernini"],"salary":["33370"],"height":["1.57"],"still_hired":["true"],"avg_worked_seconds":["349086555"],"job_positions":["Senior Team Lead"],"is_rehired":["true","false","false"],"salary_change":["10.99","-5.17"]} +{ "index" : {} } +{"birth_date":["1954-05-30T00:00:00Z"],"emp_no":["10057"],"first_name":["Ebbe"],"gender":["F"],"hire_date":["1992-01-15T00:00:00Z"],"languages":["4"],"last_name":["Callaway"],"salary":["27215"],"height":["1.59"],"still_hired":["true"],"avg_worked_seconds":["324356269"],"job_positions":["Python Developer","Head Human Resources"],"salary_change":["-6.73","-2.43","-5.27","1.03"]} +{ "index" : {} } +{"birth_date":["1954-10-01T00:00:00Z"],"emp_no":["10058"],"first_name":["Berhard"],"gender":["M"],"hire_date":["1987-04-13T00:00:00Z"],"languages":["3"],"last_name":["McFarlin"],"salary":["38376"],"height":["1.83"],"still_hired":["false"],"avg_worked_seconds":["268378108"],"job_positions":["Principal Support Engineer"],"salary_change":["-4.89"]} +{ "index" : {} } +{"birth_date":["1953-09-19T00:00:00Z"],"emp_no":["10059"],"first_name":["Alejandro"],"gender":["F"],"hire_date":["1991-06-26T00:00:00Z"],"languages":["2"],"last_name":["McAlpine"],"salary":["44307"],"height":["1.48"],"still_hired":["false"],"avg_worked_seconds":["237368465"],"job_positions":["Architect","Principal Support Engineer","Purchase Manager","Senior Team Lead"],"is_rehired":["false"],"salary_change":["5.53","13.38","-4.69","6.27"]} +{ "index" : {} } +{"birth_date":["1961-10-15T00:00:00Z"],"emp_no":["10060"],"first_name":["Breannda"],"gender":["M"],"hire_date":["1987-11-02T00:00:00Z"],"languages":["2"],"last_name":["Billingsley"],"salary":["29175"],"height":["1.42"],"still_hired":["true"],"avg_worked_seconds":["341158890"],"job_positions":["Business Analyst","Data Scientist","Senior Team Lead"],"is_rehired":["false","false","true","false"],"salary_change":["-1.76","-0.85"]} +{ "index" : {} } +{"birth_date":["1962-10-19T00:00:00Z"],"emp_no":["10061"],"first_name":["Tse"],"gender":["M"],"hire_date":["1985-09-17T00:00:00Z"],"languages":["1"],"last_name":["Herber"],"salary":["49095"],"height":["1.45"],"still_hired":["false"],"avg_worked_seconds":["327550310"],"job_positions":["Purchase Manager","Senior Team Lead"],"is_rehired":["false","true"],"salary_change":["14.39","-2.58","-0.95"]} +{ "index" : {} } +{"birth_date":["1961-11-02T00:00:00Z"],"emp_no":["10062"],"first_name":["Anoosh"],"gender":["M"],"hire_date":["1991-08-30T00:00:00Z"],"languages":["3"],"last_name":["Peyn"],"salary":["65030"],"height":["1.70"],"still_hired":["false"],"avg_worked_seconds":["203989706"],"job_positions":["Python Developer","Senior Team Lead"],"is_rehired":["false","true","true"],"salary_change":["-1.17"]} +{ "index" : {} } +{"birth_date":["1952-08-06T00:00:00Z"],"emp_no":["10063"],"first_name":["Gino"],"gender":["F"],"hire_date":["1989-04-08T00:00:00Z"],"languages":["3"],"last_name":["Leonhardt"],"salary":["52121"],"height":["1.78"],"still_hired":["true"],"avg_worked_seconds":["214068302"],"is_rehired":["true"]} +{ "index" : {} } +{"birth_date":["1959-04-07T00:00:00Z"],"emp_no":["10064"],"first_name":["Udi"],"gender":["M"],"hire_date":["1985-11-20T00:00:00Z"],"languages":["5"],"last_name":["Jansch"],"salary":["33956"],"height":["1.93"],"still_hired":["false"],"avg_worked_seconds":["307364077"],"job_positions":["Purchase Manager"],"is_rehired":["false","false","true","false"],"salary_change":["-8.66","-2.52"]} +{ "index" : {} } +{"birth_date":["1963-04-14T00:00:00Z"],"emp_no":["10065"],"first_name":["Satosi"],"gender":["M"],"hire_date":["1988-05-18T00:00:00Z"],"languages":["2"],"last_name":["Awdeh"],"salary":["50249"],"height":["1.59"],"still_hired":["false"],"avg_worked_seconds":["372660279"],"job_positions":["Business Analyst","Data Scientist","Principal Support Engineer"],"is_rehired":["false","true"],"salary_change":["-1.47","14.44","-9.81"]} +{ "index" : {} } +{"birth_date":["1952-11-13T00:00:00Z"],"emp_no":["10066"],"first_name":["Kwee"],"gender":["M"],"hire_date":["1986-02-26T00:00:00Z"],"languages":["5"],"last_name":["Schusler"],"salary":["31897"],"height":["2.10"],"still_hired":["true"],"avg_worked_seconds":["360906451"],"job_positions":["Senior Python Developer","Data Scientist","Accountant","Internship"],"is_rehired":["true","true","true"],"salary_change":["5.94"]} +{ "index" : {} } +{"birth_date":["1953-01-07T00:00:00Z"],"emp_no":["10067"],"first_name":["Claudi"],"gender":["M"],"hire_date":["1987-03-04T00:00:00Z"],"languages":["2"],"last_name":["Stavenow"],"salary":["52044"],"height":["1.77"],"still_hired":["true"],"avg_worked_seconds":["347664141"],"job_positions":["Tech Lead","Principal Support Engineer"],"is_rehired":["false","false"],"salary_change":["8.72","4.44"]} +{ "index" : {} } +{"birth_date":["1962-11-26T00:00:00Z"],"emp_no":["10068"],"first_name":["Charlene"],"gender":["M"],"hire_date":["1987-08-07T00:00:00Z"],"languages":["3"],"last_name":["Brattka"],"salary":["28941"],"height":["1.58"],"still_hired":["true"],"avg_worked_seconds":["233999584"],"job_positions":["Architect"],"is_rehired":["true"],"salary_change":["3.43","-5.61","-5.29"]} +{ "index" : {} } +{"birth_date":["1960-09-06T00:00:00Z"],"emp_no":["10069"],"first_name":["Margareta"],"gender":["F"],"hire_date":["1989-11-05T00:00:00Z"],"languages":["5"],"last_name":["Bierman"],"salary":["41933"],"height":["1.77"],"still_hired":["true"],"avg_worked_seconds":["366512352"],"job_positions":["Business Analyst","Junior Developer","Purchase Manager","Support Engineer"],"is_rehired":["false"],"salary_change":["-3.34","-6.33","6.23","-0.31"]} +{ "index" : {} } +{"birth_date":["1955-08-20T00:00:00Z"],"emp_no":["10070"],"first_name":["Reuven"],"gender":["M"],"hire_date":["1985-10-14T00:00:00Z"],"languages":["3"],"last_name":["Garigliano"],"salary":["54329"],"height":["1.77"],"still_hired":["true"],"avg_worked_seconds":["347188604"],"is_rehired":["true","true","true"],"salary_change":["-5.90"]} +{ "index" : {} } +{"birth_date":["1958-01-21T00:00:00Z"],"emp_no":["10071"],"first_name":["Hisao"],"gender":["M"],"hire_date":["1987-10-01T00:00:00Z"],"languages":["2"],"last_name":["Lipner"],"salary":["40612"],"height":["2.07"],"still_hired":["false"],"avg_worked_seconds":["306671693"],"job_positions":["Business Analyst","Reporting Analyst","Senior Team Lead"],"is_rehired":["false","false","false"],"salary_change":["-2.69"]} +{ "index" : {} } +{"birth_date":["1952-05-15T00:00:00Z"],"emp_no":["10072"],"first_name":["Hironoby"],"gender":["F"],"hire_date":["1988-07-21T00:00:00Z"],"languages":["5"],"last_name":["Sidou"],"salary":["54518"],"height":["1.82"],"still_hired":["true"],"avg_worked_seconds":["209506065"],"job_positions":["Architect","Tech Lead","Python Developer","Senior Team Lead"],"is_rehired":["false","false","true","false"],"salary_change":["11.21","-2.30","2.22","-5.44"]} +{ "index" : {} } +{"birth_date":["1954-02-23T00:00:00Z"],"emp_no":["10073"],"first_name":["Shir"],"gender":["M"],"hire_date":["1991-12-01T00:00:00Z"],"languages":["4"],"last_name":["McClurg"],"salary":["32568"],"height":["1.66"],"still_hired":["false"],"avg_worked_seconds":["314930367"],"job_positions":["Principal Support Engineer","Python Developer","Junior Developer","Purchase Manager"],"is_rehired":["true","false"],"salary_change":["-5.67"]} +{ "index" : {} } +{"birth_date":["1955-08-28T00:00:00Z"],"emp_no":["10074"],"first_name":["Mokhtar"],"gender":["F"],"hire_date":["1990-08-13T00:00:00Z"],"languages":["5"],"last_name":["Bernatsky"],"salary":["38992"],"height":["1.64"],"still_hired":["true"],"avg_worked_seconds":["382397583"],"job_positions":["Senior Python Developer","Python Developer"],"is_rehired":["true","false","false","true"],"salary_change":["6.70","1.98","-5.64","2.96"]} +{ "index" : {} } +{"birth_date":["1960-03-09T00:00:00Z"],"emp_no":["10075"],"first_name":["Gao"],"gender":["F"],"hire_date":["1987-03-19T00:00:00Z"],"languages":["5"],"last_name":["Dolinsky"],"salary":["51956"],"height":["1.94"],"still_hired":["false"],"avg_worked_seconds":["370238919"],"job_positions":["Purchase Manager"],"is_rehired":["true"],"salary_change":["9.63","-3.29","8.42"]} +{ "index" : {} } +{"birth_date":["1952-06-13T00:00:00Z"],"emp_no":["10076"],"first_name":["Erez"],"gender":["F"],"hire_date":["1985-07-09T00:00:00Z"],"languages":["3"],"last_name":["Ritzmann"],"salary":["62405"],"height":["1.83"],"still_hired":["false"],"avg_worked_seconds":["376240317"],"job_positions":["Architect","Senior Python Developer"],"is_rehired":["false"],"salary_change":["-6.90","-1.30","8.75"]} +{ "index" : {} } +{"birth_date":["1964-04-18T00:00:00Z"],"emp_no":["10077"],"first_name":["Mona"],"gender":["M"],"hire_date":["1990-03-02T00:00:00Z"],"languages":["5"],"last_name":["Azuma"],"salary":["46595"],"height":["1.68"],"still_hired":["false"],"avg_worked_seconds":["351960222"],"job_positions":["Internship"],"salary_change":["-0.01"]} +{ "index" : {} } +{"birth_date":["1959-12-25T00:00:00Z"],"emp_no":["10078"],"first_name":["Danel"],"gender":["F"],"hire_date":["1987-05-26T00:00:00Z"],"languages":["2"],"last_name":["Mondadori"],"salary":["69904"],"height":["1.81"],"still_hired":["true"],"avg_worked_seconds":["377116038"],"job_positions":["Architect","Principal Support Engineer","Internship"],"is_rehired":["true"],"salary_change":["-7.88","9.98","12.52"]} +{ "index" : {} } +{"birth_date":["1961-10-05T00:00:00Z"],"emp_no":["10079"],"first_name":["Kshitij"],"gender":["F"],"hire_date":["1986-03-27T00:00:00Z"],"languages":["2"],"last_name":["Gils"],"salary":["32263"],"height":["1.59"],"still_hired":["false"],"avg_worked_seconds":["320953330"],"is_rehired":["false"],"salary_change":["7.58"]} +{ "index" : {} } +{"birth_date":["1957-12-03T00:00:00Z"],"emp_no":["10080"],"first_name":["Premal"],"gender":["M"],"hire_date":["1985-11-19T00:00:00Z"],"languages":["5"],"last_name":["Baek"],"salary":["52833"],"height":["1.80"],"still_hired":["false"],"avg_worked_seconds":["239266137"],"job_positions":["Senior Python Developer"],"salary_change":["-4.35","7.36","5.56"]} +{ "index" : {} } +{"birth_date":["1960-12-17T00:00:00Z"],"emp_no":["10081"],"first_name":["Zhongwei"],"gender":["M"],"hire_date":["1986-10-30T00:00:00Z"],"languages":["2"],"last_name":["Rosen"],"salary":["50128"],"height":["1.44"],"still_hired":["true"],"avg_worked_seconds":["321375511"],"job_positions":["Accountant","Internship"],"is_rehired":["false","false","false"]} +{ "index" : {} } +{"birth_date":["1963-09-09T00:00:00Z"],"emp_no":["10082"],"first_name":["Parviz"],"gender":["M"],"hire_date":["1990-01-03T00:00:00Z"],"languages":["4"],"last_name":["Lortz"],"salary":["49818"],"height":["1.61"],"still_hired":["false"],"avg_worked_seconds":["232522994"],"job_positions":["Principal Support Engineer"],"is_rehired":["false"],"salary_change":["1.19","-3.39"]} +{ "index" : {} } +{"birth_date":["1959-07-23T00:00:00Z"],"emp_no":["10083"],"first_name":["Vishv"],"gender":["M"],"hire_date":["1987-03-31T00:00:00Z"],"languages":["1"],"last_name":["Zockler"],"salary":["39110"],"height":["1.42"],"still_hired":["false"],"avg_worked_seconds":["331236443"],"job_positions":["Head Human Resources"]} +{ "index" : {} } +{"birth_date":["1960-05-25T00:00:00Z"],"emp_no":["10084"],"first_name":["Tuval"],"gender":["M"],"hire_date":["1995-12-15T00:00:00Z"],"languages":["1"],"last_name":["Kalloufi"],"salary":["28035"],"height":["1.51"],"still_hired":["true"],"avg_worked_seconds":["359067056"],"job_positions":["Principal Support Engineer"],"is_rehired":["false"]} +{ "index" : {} } +{"birth_date":["1962-11-07T00:00:00Z"],"emp_no":["10085"],"first_name":["Kenroku"],"gender":["M"],"hire_date":["1994-04-09T00:00:00Z"],"languages":["5"],"last_name":["Malabarba"],"salary":["35742"],"height":["2.01"],"still_hired":["true"],"avg_worked_seconds":["353404008"],"job_positions":["Senior Python Developer","Business Analyst","Tech Lead","Accountant"],"salary_change":["11.67","6.75","8.40"]} +{ "index" : {} } +{"birth_date":["1962-11-19T00:00:00Z"],"emp_no":["10086"],"first_name":["Somnath"],"gender":["M"],"hire_date":["1990-02-16T00:00:00Z"],"languages":["1"],"last_name":["Foote"],"salary":["68547"],"height":["1.74"],"still_hired":["true"],"avg_worked_seconds":["328580163"],"job_positions":["Senior Python Developer"],"is_rehired":["false","true"],"salary_change":["13.61"]} +{ "index" : {} } +{"birth_date":["1959-07-23T00:00:00Z"],"emp_no":["10087"],"first_name":["Xinglin"],"gender":["F"],"hire_date":["1986-09-08T00:00:00Z"],"languages":["5"],"last_name":["Eugenio"],"salary":["32272"],"height":["1.74"],"still_hired":["true"],"avg_worked_seconds":["305782871"],"job_positions":["Junior Developer","Internship"],"is_rehired":["false","false"],"salary_change":["-2.05"]} +{ "index" : {} } +{"birth_date":["1954-02-25T00:00:00Z"],"emp_no":["10088"],"first_name":["Jungsoon"],"gender":["F"],"hire_date":["1988-09-02T00:00:00Z"],"languages":["5"],"last_name":["Syrzycki"],"salary":["39638"],"height":["1.91"],"still_hired":["false"],"avg_worked_seconds":["330714423"],"job_positions":["Reporting Analyst","Business Analyst","Tech Lead"],"is_rehired":["true"]} +{ "index" : {} } +{"birth_date":["1963-03-21T00:00:00Z"],"emp_no":["10089"],"first_name":["Sudharsan"],"gender":["F"],"hire_date":["1986-08-12T00:00:00Z"],"languages":["4"],"last_name":["Flasterstein"],"salary":["43602"],"height":["1.57"],"still_hired":["true"],"avg_worked_seconds":["232951673"],"job_positions":["Junior Developer","Accountant"],"is_rehired":["true","false","false","false"]} +{ "index" : {} } +{"birth_date":["1961-05-30T00:00:00Z"],"emp_no":["10090"],"first_name":["Kendra"],"gender":["M"],"hire_date":["1986-03-14T00:00:00Z"],"languages":["2"],"last_name":["Hofting"],"salary":["44956"],"height":["2.03"],"still_hired":["true"],"avg_worked_seconds":["212460105"],"is_rehired":["false","false","false","true"],"salary_change":["7.15","-1.85","3.60"]} +{ "index" : {} } +{"birth_date":["1955-10-04T00:00:00Z"],"emp_no":["10091"],"first_name":["Amabile"],"gender":["M"],"hire_date":["1992-11-18T00:00:00Z"],"languages":["3"],"last_name":["Gomatam"],"salary":["38645"],"height":["2.09"],"still_hired":["true"],"avg_worked_seconds":["242582807"],"job_positions":["Reporting Analyst","Python Developer"],"is_rehired":["true","true","false","false"],"salary_change":["-9.23","7.50","5.85","5.19"]} +{ "index" : {} } +{"birth_date":["1964-10-18T00:00:00Z"],"emp_no":["10092"],"first_name":["Valdiodio"],"gender":["F"],"hire_date":["1989-09-22T00:00:00Z"],"languages":["1"],"last_name":["Niizuma"],"salary":["25976"],"height":["1.75"],"still_hired":["false"],"avg_worked_seconds":["313407352"],"job_positions":["Junior Developer","Accountant"],"is_rehired":["false","false","true","true"],"salary_change":["8.78","0.39","-6.77","8.30"]} +{ "index" : {} } +{"birth_date":["1964-06-11T00:00:00Z"],"emp_no":["10093"],"first_name":["Sailaja"],"gender":["M"],"hire_date":["1996-11-05T00:00:00Z"],"languages":["3"],"last_name":["Desikan"],"salary":["45656"],"height":["1.69"],"still_hired":["false"],"avg_worked_seconds":["315904921"],"job_positions":["Reporting Analyst","Tech Lead","Principal Support Engineer","Purchase Manager"],"salary_change":["-0.88"]} +{ "index" : {} } +{"birth_date":["1957-05-25T00:00:00Z"],"emp_no":["10094"],"first_name":["Arumugam"],"gender":["F"],"hire_date":["1987-04-18T00:00:00Z"],"languages":["5"],"last_name":["Ossenbruggen"],"salary":["66817"],"height":["2.10"],"still_hired":["false"],"avg_worked_seconds":["332920135"],"job_positions":["Senior Python Developer","Principal Support Engineer","Accountant"],"is_rehired":["true","false","true"],"salary_change":["2.22","7.92"]} +{ "index" : {} } +{"birth_date":["1965-01-03T00:00:00Z"],"emp_no":["10095"],"first_name":["Hilari"],"gender":["M"],"hire_date":["1986-07-15T00:00:00Z"],"languages":["4"],"last_name":["Morton"],"salary":["37702"],"height":["1.55"],"still_hired":["false"],"avg_worked_seconds":["321850475"],"is_rehired":["true","true","false","false"],"salary_change":["-3.93","-6.66"]} +{ "index" : {} } +{"birth_date":["1954-09-16T00:00:00Z"],"emp_no":["10096"],"first_name":["Jayson"],"gender":["M"],"hire_date":["1990-01-14T00:00:00Z"],"languages":["4"],"last_name":["Mandell"],"salary":["43889"],"height":["1.94"],"still_hired":["false"],"avg_worked_seconds":["204381503"],"job_positions":["Architect","Reporting Analyst"],"is_rehired":["false","false","false"]} +{ "index" : {} } +{"birth_date":["1952-02-27T00:00:00Z"],"emp_no":["10097"],"first_name":["Remzi"],"gender":["M"],"hire_date":["1990-09-15T00:00:00Z"],"languages":["3"],"last_name":["Waschkowski"],"salary":["71165"],"height":["1.53"],"still_hired":["false"],"avg_worked_seconds":["206258084"],"job_positions":["Reporting Analyst","Tech Lead"],"is_rehired":["true","false"],"salary_change":["-1.12"]} +{ "index" : {} } +{"birth_date":["1961-09-23T00:00:00Z"],"emp_no":["10098"],"first_name":["Sreekrishna"],"gender":["F"],"hire_date":["1985-05-13T00:00:00Z"],"languages":["4"],"last_name":["Servieres"],"salary":["44817"],"height":["2.00"],"still_hired":["false"],"avg_worked_seconds":["272392146"],"job_positions":["Architect","Internship","Senior Team Lead"],"is_rehired":["false"],"salary_change":["-2.83","8.31","4.38"]} +{ "index" : {} } +{"birth_date":["1956-05-25T00:00:00Z"],"emp_no":["10099"],"first_name":["Valter"],"gender":["F"],"hire_date":["1988-10-18T00:00:00Z"],"languages":["2"],"last_name":["Sullins"],"salary":["73578"],"height":["1.81"],"still_hired":["true"],"avg_worked_seconds":["377713748"],"is_rehired":["true","true"],"salary_change":["10.71","14.26","-8.78","-3.98"]} +{ "index" : {} } +{"birth_date":["1953-04-21T00:00:00Z"],"emp_no":["10100"],"first_name":["Hironobu"],"gender":["F"],"hire_date":["1987-09-21T00:00:00Z"],"languages":["4"],"last_name":["Haraldson"],"salary":["68431"],"height":["1.77"],"still_hired":["true"],"avg_worked_seconds":["223910853"],"job_positions":["Purchase Manager"],"is_rehired":["false","true","true","false"],"salary_change":["13.97","-7.49"]} +