diff --git a/integ-test/build.gradle b/integ-test/build.gradle index 2b1d75bd85..685bd0b360 100644 --- a/integ-test/build.gradle +++ b/integ-test/build.gradle @@ -128,9 +128,6 @@ task integTestWithNewEngine(type: RestIntegTestTask) { // Skip old semantic analyzer IT because analyzer in new engine has different behavior exclude 'com/amazon/opendistroforelasticsearch/sql/legacy/QueryAnalysisIT.class' - // Skip this IT to avoid breaking tests due to inconsistency in JDBC schema - exclude 'com/amazon/opendistroforelasticsearch/sql/legacy/AggregationExpressionIT.class' - // Skip this IT because all assertions are against explain output exclude 'com/amazon/opendistroforelasticsearch/sql/legacy/OrderIT.class' } diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/QueryIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/QueryIT.java index 5cec676835..1e634ff679 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/QueryIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/legacy/QueryIT.java @@ -187,7 +187,6 @@ public void selectAllWithFieldAndGroupByReverseOrder() throws IOException { checkSelectAllAndFieldAggregationResponseSize(response, "age"); } - @Ignore("This failed because there is no alias field in schema of new engine default formatter") @Test public void selectFieldWithAliasAndGroupBy() { String response = diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java index 1f69188598..3e603963c6 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/DateTimeFunctionIT.java @@ -136,11 +136,11 @@ public void testDay() throws IOException { @Test public void testDayName() throws IOException { JSONObject result = executeQuery("select dayname(date('2020-09-16'))"); - verifySchema(result, schema("dayname(date('2020-09-16'))", null, "string")); + verifySchema(result, schema("dayname(date('2020-09-16'))", null, "keyword")); verifyDataRows(result, rows("Wednesday")); result = executeQuery("select dayname('2020-09-16')"); - verifySchema(result, schema("dayname('2020-09-16')", null, "string")); + verifySchema(result, schema("dayname('2020-09-16')", null, "keyword")); verifyDataRows(result, rows("Wednesday")); } @@ -256,11 +256,11 @@ public void testMonth() throws IOException { @Test public void testMonthName() throws IOException { JSONObject result = executeQuery("select monthname(date('2020-09-16'))"); - verifySchema(result, schema("monthname(date('2020-09-16'))", null, "string")); + verifySchema(result, schema("monthname(date('2020-09-16'))", null, "keyword")); verifyDataRows(result, rows("September")); result = executeQuery("select monthname('2020-09-16')"); - verifySchema(result, schema("monthname('2020-09-16')", null, "string")); + verifySchema(result, schema("monthname('2020-09-16')", null, "keyword")); verifyDataRows(result, rows("September")); } @@ -378,12 +378,12 @@ public void testWeek() throws IOException { void verifyDateFormat(String date, String type, String format, String formatted) throws IOException { String query = String.format("date_format(%s('%s'), '%s')", type, date, format); JSONObject result = executeQuery("select " + query); - verifySchema(result, schema(query, null, "string")); + verifySchema(result, schema(query, null, "keyword")); verifyDataRows(result, rows(formatted)); query = String.format("date_format('%s', '%s')", date, format); result = executeQuery("select " + query); - verifySchema(result, schema(query, null, "string")); + verifySchema(result, schema(query, null, "keyword")); verifyDataRows(result, rows(formatted)); } diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/JdbcFormatIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/JdbcFormatIT.java new file mode 100644 index 0000000000..51cf961ca5 --- /dev/null +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/JdbcFormatIT.java @@ -0,0 +1,58 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package com.amazon.opendistroforelasticsearch.sql.sql; + +import static com.amazon.opendistroforelasticsearch.sql.legacy.TestsConstants.TEST_INDEX_BANK; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.schema; +import static com.amazon.opendistroforelasticsearch.sql.util.MatcherUtils.verifySchema; + +import com.amazon.opendistroforelasticsearch.sql.legacy.SQLIntegTestCase; +import org.json.JSONObject; +import org.junit.jupiter.api.Test; + +public class JdbcFormatIT extends SQLIntegTestCase { + + @Override + protected void init() throws Exception { + loadIndex(Index.BANK); + } + + @Test + public void testSimpleDataTypesInSchema() { + JSONObject response = new JSONObject(executeQuery( + "SELECT account_number, address, age, birthdate, city, male, state " + + "FROM " + TEST_INDEX_BANK, "jdbc")); + + verifySchema(response, + schema("account_number", "long"), + schema("address", "text"), + schema("age", "integer"), + schema("birthdate", "timestamp"), + schema("city", "keyword"), + schema("male", "boolean"), + schema("state", "text")); + } + + @Test + public void testAliasInSchema() { + JSONObject response = new JSONObject(executeQuery( + "SELECT account_number AS acc FROM " + TEST_INDEX_BANK, "jdbc")); + + verifySchema(response, schema("acc", "acc", "long")); + } + +} diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/MathematicalFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/MathematicalFunctionIT.java index f24de89146..7c55cbcc8e 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/MathematicalFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/MathematicalFunctionIT.java @@ -43,11 +43,11 @@ public void init() throws Exception { @Test public void testConv() throws IOException { JSONObject result = executeQuery("select conv(11, 10, 16)"); - verifySchema(result, schema("conv(11, 10, 16)", null, "string")); + verifySchema(result, schema("conv(11, 10, 16)", null, "keyword")); verifyDataRows(result, rows("b")); result = executeQuery("select conv(11, 16, 10)"); - verifySchema(result, schema("conv(11, 16, 10)", null, "string")); + verifySchema(result, schema("conv(11, 16, 10)", null, "keyword")); verifyDataRows(result, rows("17")); } diff --git a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/TextFunctionIT.java b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/TextFunctionIT.java index 1cd3285fe8..d972a10c36 100644 --- a/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/TextFunctionIT.java +++ b/integ-test/src/test/java/com/amazon/opendistroforelasticsearch/sql/sql/TextFunctionIT.java @@ -60,59 +60,59 @@ public void testRegexp() throws IOException { @Test public void testSubstr() throws IOException { - verifyQuery("substr('hello', 2)", "string", "ello"); - verifyQuery("substr('hello', 2, 2)", "string", "el"); + verifyQuery("substr('hello', 2)", "keyword", "ello"); + verifyQuery("substr('hello', 2, 2)", "keyword", "el"); } @Test public void testSubstring() throws IOException { - verifyQuery("substring('hello', 2)", "string", "ello"); - verifyQuery("substring('hello', 2, 2)", "string", "el"); + verifyQuery("substring('hello', 2)", "keyword", "ello"); + verifyQuery("substring('hello', 2, 2)", "keyword", "el"); } @Test public void testUpper() throws IOException { - verifyQuery("upper('hello')", "string", "HELLO"); - verifyQuery("upper('HELLO')", "string", "HELLO"); + verifyQuery("upper('hello')", "keyword", "HELLO"); + verifyQuery("upper('HELLO')", "keyword", "HELLO"); } @Test public void testLower() throws IOException { - verifyQuery("lower('hello')", "string", "hello"); - verifyQuery("lower('HELLO')", "string", "hello"); + verifyQuery("lower('hello')", "keyword", "hello"); + verifyQuery("lower('HELLO')", "keyword", "hello"); } @Test public void testTrim() throws IOException { - verifyQuery("trim(' hello')", "string", "hello"); - verifyQuery("trim('hello ')", "string", "hello"); - verifyQuery("trim(' hello ')", "string", "hello"); + verifyQuery("trim(' hello')", "keyword", "hello"); + verifyQuery("trim('hello ')", "keyword", "hello"); + verifyQuery("trim(' hello ')", "keyword", "hello"); } @Test public void testRtrim() throws IOException { - verifyQuery("rtrim(' hello')", "string", " hello"); - verifyQuery("rtrim('hello ')", "string", "hello"); - verifyQuery("rtrim(' hello ')", "string", " hello"); + verifyQuery("rtrim(' hello')", "keyword", " hello"); + verifyQuery("rtrim('hello ')", "keyword", "hello"); + verifyQuery("rtrim(' hello ')", "keyword", " hello"); } @Test public void testLtrim() throws IOException { - verifyQuery("ltrim(' hello')", "string", "hello"); - verifyQuery("ltrim('hello ')", "string", "hello "); - verifyQuery("ltrim(' hello ')", "string", "hello "); + verifyQuery("ltrim(' hello')", "keyword", "hello"); + verifyQuery("ltrim('hello ')", "keyword", "hello "); + verifyQuery("ltrim(' hello ')", "keyword", "hello "); } @Test public void testConcat() throws IOException { - verifyQuery("concat('hello', 'world')", "string", "helloworld"); - verifyQuery("concat('', 'hello')", "string", "hello"); + verifyQuery("concat('hello', 'world')", "keyword", "helloworld"); + verifyQuery("concat('', 'hello')", "keyword", "hello"); } @Test public void testConcat_ws() throws IOException { - verifyQuery("concat_ws(',', 'hello', 'world')", "string", "hello,world"); - verifyQuery("concat_ws(',', '', 'hello')", "string", ",hello"); + verifyQuery("concat_ws(',', 'hello', 'world')", "keyword", "hello,world"); + verifyQuery("concat_ws(',', '', 'hello')", "keyword", ",hello"); } @Test diff --git a/legacy/src/main/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryAction.java b/legacy/src/main/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryAction.java index 935c9c849f..565fb69638 100644 --- a/legacy/src/main/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryAction.java +++ b/legacy/src/main/java/com/amazon/opendistroforelasticsearch/sql/legacy/plugin/RestSQLQueryAction.java @@ -28,8 +28,8 @@ import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.ExplainResponse; import com.amazon.opendistroforelasticsearch.sql.planner.physical.PhysicalPlan; import com.amazon.opendistroforelasticsearch.sql.protocol.response.QueryResult; +import com.amazon.opendistroforelasticsearch.sql.protocol.response.format.JdbcResponseFormatter; import com.amazon.opendistroforelasticsearch.sql.protocol.response.format.JsonResponseFormatter; -import com.amazon.opendistroforelasticsearch.sql.protocol.response.format.SimpleJsonResponseFormatter; import com.amazon.opendistroforelasticsearch.sql.sql.SQLService; import com.amazon.opendistroforelasticsearch.sql.sql.config.SQLServiceConfig; import com.amazon.opendistroforelasticsearch.sql.sql.domain.SQLQueryRequest; @@ -149,9 +149,8 @@ public void onFailure(Exception e) { }; } - // TODO: duplicate code here as in RestPPLQueryAction private ResponseListener createQueryResponseListener(RestChannel channel) { - SimpleJsonResponseFormatter formatter = new SimpleJsonResponseFormatter(PRETTY); + JdbcResponseFormatter formatter = new JdbcResponseFormatter(PRETTY); return new ResponseListener() { @Override public void onResponse(QueryResponse response) { diff --git a/protocol/build.gradle b/protocol/build.gradle index 54e6bb1b89..3bb6b58ff1 100644 --- a/protocol/build.gradle +++ b/protocol/build.gradle @@ -15,6 +15,7 @@ dependencies { compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.4' implementation 'com.google.code.gson:gson:2.8.6' compile project(':core') + compile project(':elasticsearch') testImplementation('org.junit.jupiter:junit-jupiter:5.6.2') testCompile group: 'org.hamcrest', name: 'hamcrest-library', version: '2.1' @@ -49,9 +50,13 @@ jacocoTestCoverageVerification { violationRules { rule { limit { + counter = 'LINE' + minimum = 1.0 + } + limit { + counter = 'BRANCH' minimum = 1.0 } - } } afterEvaluate { diff --git a/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/QueryResult.java b/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/QueryResult.java index cc8b4d73bd..83a09366b9 100644 --- a/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/QueryResult.java +++ b/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/QueryResult.java @@ -23,6 +23,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import lombok.Getter; import lombok.RequiredArgsConstructor; /** @@ -31,6 +32,8 @@ */ @RequiredArgsConstructor public class QueryResult implements Iterable { + + @Getter private final ExecutionEngine.Schema schema; /** diff --git a/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JdbcResponseFormatter.java b/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JdbcResponseFormatter.java new file mode 100644 index 0000000000..a7c798551d --- /dev/null +++ b/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JdbcResponseFormatter.java @@ -0,0 +1,145 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package com.amazon.opendistroforelasticsearch.sql.protocol.response.format; + +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.ARRAY; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRUCT; +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT; +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT_KEYWORD; + +import com.amazon.opendistroforelasticsearch.sql.common.antlr.SyntaxCheckException; +import com.amazon.opendistroforelasticsearch.sql.data.type.ExprType; +import com.amazon.opendistroforelasticsearch.sql.exception.QueryEngineException; +import com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.Schema; +import com.amazon.opendistroforelasticsearch.sql.protocol.response.QueryResult; +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Singular; + +/** + * JDBC formatter that formats both normal or error response exactly same way as legacy code to + * avoid impact on client side. The only difference is a new "version" that indicates the response + * was produced by new query engine. + */ +public class JdbcResponseFormatter extends JsonResponseFormatter { + + public JdbcResponseFormatter(Style style) { + super(style); + } + + @Override + protected Object buildJsonObject(QueryResult response) { + JdbcResponse.JdbcResponseBuilder json = JdbcResponse.builder(); + + // Fetch schema and data rows + response.getSchema().getColumns().forEach(col -> json.column(fetchColumn(col))); + json.datarows(fetchDataRows(response)); + + // Populate other fields + json.total(response.size()) + .size(response.size()) + .status(200); + + return json.build(); + } + + @Override + public String format(Throwable t) { + Error error = new Error( + t.getClass().getSimpleName(), + t.getMessage(), + t.getMessage()); + return jsonify(new JdbcErrorResponse(error, getStatus(t))); + } + + private Column fetchColumn(Schema.Column col) { + return new Column(col.getName(), col.getAlias(), convertToLegacyType(col.getExprType())); + } + + /** + * Convert type that exists in both legacy and new engine but has different name. + * Return old type name to avoid breaking impact on client-side. + */ + private String convertToLegacyType(ExprType type) { + if (type == ES_TEXT || type == ES_TEXT_KEYWORD) { + return "text"; + } else if (type == STRING) { + return "keyword"; + } else if (type == STRUCT) { + return "object"; + } else if (type == ARRAY) { + return "nested"; + } else { + return type.typeName().toLowerCase(); + } + } + + private Object[][] fetchDataRows(QueryResult response) { + Object[][] rows = new Object[response.size()][]; + int i = 0; + for (Object[] values : response) { + rows[i++] = values; + } + return rows; + } + + private int getStatus(Throwable t) { + return (t instanceof SyntaxCheckException + || t instanceof QueryEngineException) ? 400 : 503; + } + + /** + * org.json requires these inner data classes be public (and static) + */ + @Builder + @Getter + public static class JdbcResponse { + @Singular("column") + private final List schema; + private final Object[][] datarows; + private final long total; + private final long size; + private final int status; + } + + @RequiredArgsConstructor + @Getter + public static class Column { + private final String name; + private final String alias; + private final String type; + } + + @RequiredArgsConstructor + @Getter + public static class JdbcErrorResponse { + private final Error error; + private final int status; + } + + @RequiredArgsConstructor + @Getter + public static class Error { + private final String type; + private final String reason; + private final String details; + } + +} diff --git a/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JsonResponseFormatter.java b/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JsonResponseFormatter.java index 4f3706341b..e901aca811 100644 --- a/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JsonResponseFormatter.java +++ b/protocol/src/main/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JsonResponseFormatter.java @@ -71,7 +71,7 @@ public String format(Throwable t) { */ protected abstract Object buildJsonObject(R response); - private String jsonify(Object jsonObject) { + protected String jsonify(Object jsonObject) { return AccessController.doPrivileged((PrivilegedAction) () -> (style == PRETTY) ? PRETTY_PRINT_GSON.toJson(jsonObject) : GSON.toJson(jsonObject)); } diff --git a/protocol/src/test/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JdbcResponseFormatterTest.java b/protocol/src/test/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JdbcResponseFormatterTest.java new file mode 100644 index 0000000000..2705b31d50 --- /dev/null +++ b/protocol/src/test/java/com/amazon/opendistroforelasticsearch/sql/protocol/response/format/JdbcResponseFormatterTest.java @@ -0,0 +1,163 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +package com.amazon.opendistroforelasticsearch.sql.protocol.response.format; + +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_MISSING; +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.LITERAL_NULL; +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.stringValue; +import static com.amazon.opendistroforelasticsearch.sql.data.model.ExprValueUtils.tupleValue; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.ARRAY; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.INTEGER; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRING; +import static com.amazon.opendistroforelasticsearch.sql.data.type.ExprCoreType.STRUCT; +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT; +import static com.amazon.opendistroforelasticsearch.sql.elasticsearch.data.type.ElasticsearchDataType.ES_TEXT_KEYWORD; +import static com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.Schema; +import static com.amazon.opendistroforelasticsearch.sql.executor.ExecutionEngine.Schema.Column; +import static com.amazon.opendistroforelasticsearch.sql.protocol.response.format.JsonResponseFormatter.Style.COMPACT; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.amazon.opendistroforelasticsearch.sql.common.antlr.SyntaxCheckException; +import com.amazon.opendistroforelasticsearch.sql.data.model.ExprTupleValue; +import com.amazon.opendistroforelasticsearch.sql.exception.SemanticCheckException; +import com.amazon.opendistroforelasticsearch.sql.protocol.response.QueryResult; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonParser; +import java.util.Arrays; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class JdbcResponseFormatterTest { + + private final JdbcResponseFormatter formatter = new JdbcResponseFormatter(COMPACT); + + @Test + void format_response() { + QueryResult response = new QueryResult( + new Schema(ImmutableList.of( + new Column("name", "name", STRING), + new Column("address1", "address1", ES_TEXT), + new Column("address2", "address2", ES_TEXT_KEYWORD), + new Column("location", "location", STRUCT), + new Column("employer", "employer", ARRAY), + new Column("age", "age", INTEGER))), + ImmutableList.of( + tupleValue(ImmutableMap.builder() + .put("name", "John") + .put("address1", "Seattle") + .put("address2", "WA") + .put("location", ImmutableMap.of("x", "1", "y", "2")) + .put("employments", ImmutableList.of( + ImmutableMap.of("name", "Amazon"), + ImmutableMap.of("name", "AWS"))) + .put("age", 20) + .build()))); + + assertJsonEquals( + "{" + + "\"schema\":[" + + "{\"name\":\"name\",\"alias\":\"name\",\"type\":\"keyword\"}," + + "{\"name\":\"address1\",\"alias\":\"address1\",\"type\":\"text\"}," + + "{\"name\":\"address2\",\"alias\":\"address2\",\"type\":\"text\"}," + + "{\"name\":\"location\",\"alias\":\"location\",\"type\":\"object\"}," + + "{\"name\":\"employer\",\"alias\":\"employer\",\"type\":\"nested\"}," + + "{\"name\":\"age\",\"alias\":\"age\",\"type\":\"integer\"}" + + "]," + + "\"datarows\":[" + + "[\"John\",\"Seattle\",\"WA\",{\"x\":\"1\",\"y\":\"2\"}," + + "[{\"name\":\"Amazon\"}," + "{\"name\":\"AWS\"}]," + + "20]]," + + "\"total\":1," + + "\"size\":1," + + "\"status\":200}", + formatter.format(response)); + } + + @Test + void format_response_with_missing_and_null_value() { + QueryResult response = + new QueryResult( + new Schema(ImmutableList.of( + new Column("name", null, STRING), + new Column("age", null, INTEGER))), + Arrays.asList( + ExprTupleValue.fromExprValueMap( + ImmutableMap.of("name", stringValue("John"), "age", LITERAL_MISSING)), + ExprTupleValue.fromExprValueMap( + ImmutableMap.of("name", stringValue("Allen"), "age", LITERAL_NULL)), + tupleValue(ImmutableMap.of("name", "Smith", "age", 30)))); + + assertEquals( + "{\"schema\":[{\"name\":\"name\",\"type\":\"keyword\"}," + + "{\"name\":\"age\",\"type\":\"integer\"}]," + + "\"datarows\":[[\"John\",null],[\"Allen\",null]," + + "[\"Smith\",30]],\"total\":3,\"size\":3,\"status\":200}", + formatter.format(response)); + } + + @Test + void format_client_error_response_due_to_syntax_exception() { + assertJsonEquals( + "{\"error\":" + + "{\"" + + "type\":\"SyntaxCheckException\"," + + "\"reason\":\"Invalid query syntax\"," + + "\"details\":\"Invalid query syntax\"" + + "}," + + "\"status\":400}", + formatter.format(new SyntaxCheckException("Invalid query syntax")) + ); + } + + @Test + void format_client_error_response_due_to_semantic_exception() { + assertJsonEquals( + "{\"error\":" + + "{\"" + + "type\":\"SemanticCheckException\"," + + "\"reason\":\"Invalid query semantics\"," + + "\"details\":\"Invalid query semantics\"" + + "}," + + "\"status\":400}", + formatter.format(new SemanticCheckException("Invalid query semantics")) + ); + } + + @Test + void format_server_error_response() { + assertJsonEquals( + "{\"error\":" + + "{\"" + + "type\":\"IllegalStateException\"," + + "\"reason\":\"Execution error\"," + + "\"details\":\"Execution error\"" + + "}," + + "\"status\":503}", + formatter.format(new IllegalStateException("Execution error")) + ); + } + + private static void assertJsonEquals(String expected, String actual) { + assertEquals( + JsonParser.parseString(expected), + JsonParser.parseString(actual)); + } + +} \ No newline at end of file