"Parsed property: \"" + key + "\" = \"" + stringValue + "\"");
properties.put(key, stringValue);
}
}
diff --git a/src/main/java/com/exasol/adapter/request/parser/DataTypeParser.java b/src/main/java/com/exasol/adapter/request/parser/DataTypeParser.java
new file mode 100644
index 0000000..1244b06
--- /dev/null
+++ b/src/main/java/com/exasol/adapter/request/parser/DataTypeParser.java
@@ -0,0 +1,108 @@
+package com.exasol.adapter.request.parser;
+
+import static com.exasol.adapter.request.parser.DataTypeProperty.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import com.exasol.adapter.metadata.DataType;
+import com.exasol.adapter.metadata.DataType.ExaCharset;
+import com.exasol.errorreporting.ExaError;
+
+import jakarta.json.JsonArray;
+import jakarta.json.JsonObject;
+
+/**
+ * Starting with major version 8 Exasol database uses the capabilities reported by each virtual schema to provide select
+ * list data types for each push down request. Based on this information the JDBC virtual schemas no longer need to
+ * infer the data types of the result set by inspecting its values. Instead the JDBC virtual schemas can now use the
+ * information provided by the database.
+ *
+ *
+ * Class {@link DataTypeParser} parses the data types from json.
+ *
+ */
+public class DataTypeParser {
+
+ /**
+ * @return new instance of {@link DataTypeParser}
+ */
+ public static DataTypeParser create() {
+ return new DataTypeParser();
+ }
+
+ private DataTypeParser() {
+ }
+
+ /**
+ * @param jsonArray {@link JsonArray} containing the data types to parse
+ * @return list of parsed data types
+ */
+ public List parse(final JsonArray jsonArray) {
+ return jsonArray.getValuesAs(JsonObject.class).stream() //
+ .map(this::datatype) //
+ .collect(Collectors.toList());
+ }
+
+ private DataType datatype(final JsonObject entry) {
+ if (!entry.containsKey(TYPE.key)) {
+ throw new DataTypeParserException(ExaError.messageBuilder("E-VSCOMJAVA-40") //
+ .message("Unspecified datatype in {{json}}.", entry.toString()) //
+ .ticketMitigation().toString());
+ }
+ switch (TYPE.get(entry)) {
+ case "DECIMAL":
+ // should we accept at least if both precision *and* scale missing and use default value -1 for both?
+ return DataType.createDecimal(PRECISION.get(entry), SCALE.get(entry));
+ case "DOUBLE":
+ return DataType.createDouble();
+ case "VARCHAR":
+ return DataType.createVarChar(SIZE.get(entry), CHARSET.get(entry, ExaCharset.UTF8));
+ case "CHAR":
+ return DataType.createChar(SIZE.get(entry), CHARSET.get(entry, ExaCharset.UTF8));
+ case "DATE":
+ return DataType.createDate();
+ case "TIMESTAMP":
+ return DataType.createTimestamp(WITH_LOCAL_TIMEZONE.get(entry, false));
+ case "BOOLEAN":
+ return DataType.createBool();
+ case "GEOMETRY":
+ return DataType.createGeometry(SCALE.get(entry, 0));
+ case "HASHTYPE":
+ return DataType.createHashtype(BYTESIZE.get(entry, 16));
+ case "INTERVAL":
+ return DataType.createIntervalDaySecond(PRECISION.get(entry, 2), FRACTION.get(entry, 3));
+ case "UNSUPPORTED": // fall through
+ default:
+ throw new DataTypeParserException(ExaError.messageBuilder("E-VSCOMJAVA-37") //
+ .message("Unsupported datatype {{datatype}}.", TYPE.get(entry)) //
+ .ticketMitigation().toString());
+ }
+ }
+
+ /**
+ * Signal an error during parsing data types from json.
+ */
+ public static class DataTypeParserException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Create a new instance of {@link DataTypeParserException}
+ *
+ * @param message message of the exception
+ */
+ public DataTypeParserException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Create a new instance of {@link DataTypeParserException}
+ *
+ * @param message message of the exception
+ * @param exception inner exception being the cause of the current exception
+ */
+ public DataTypeParserException(final String message, final Exception exception) {
+ super(message, exception);
+ }
+ }
+}
diff --git a/src/main/java/com/exasol/adapter/request/parser/DataTypeProperty.java b/src/main/java/com/exasol/adapter/request/parser/DataTypeProperty.java
new file mode 100644
index 0000000..5b06b93
--- /dev/null
+++ b/src/main/java/com/exasol/adapter/request/parser/DataTypeProperty.java
@@ -0,0 +1,98 @@
+package com.exasol.adapter.request.parser;
+
+import com.exasol.adapter.metadata.DataType.ExaCharset;
+import com.exasol.adapter.request.parser.DataTypeParser.DataTypeParserException;
+import com.exasol.errorreporting.ExaError;
+
+import jakarta.json.JsonObject;
+
+class DataTypeProperty {
+
+ static final StringProperty TYPE = new StringProperty("type");
+ static final IntProperty PRECISION = new IntProperty("precision");
+ static final IntProperty SCALE = new IntProperty("scale");
+ static final IntProperty SIZE = new IntProperty("size");
+ static final CharsetProperty CHARSET = new CharsetProperty("characterSet");
+ // These can only be verified by using exasol-virtual-schema
+ // as most other virtual schemas do not support data types using any of these properties
+
+ // to do: verify with data type TIMESTAMP!
+ static final BooleanProperty WITH_LOCAL_TIMEZONE = new BooleanProperty("withLocalTimeZone");
+ static final IntProperty FRACTION = new IntProperty("fraction"); // to do: verify with data type INTERVAL!
+ static final IntProperty BYTESIZE = new IntProperty("byteSize"); // to do: verify with data type HASHTYPE!
+
+ protected final String key;
+ private final DataTypeProperty.JsonGetter getter;
+
+ DataTypeProperty(final String key, final DataTypeProperty.JsonGetter getter) {
+ this.key = key;
+ this.getter = getter;
+ }
+
+ public T get(final JsonObject json) {
+ return get(json, null);
+ }
+
+ public T get(final JsonObject json, final T defaultValue) {
+ if (json.containsKey(this.key)) {
+ try {
+ return this.getter.apply(json, this.key);
+ } catch (final Exception exception) {
+ throw new DataTypeParserException(ExaError.messageBuilder("E-VSCOMJAVA-39") //
+ .message("Datatype {{datatype}}, property {{property}}: Illegal value {{value}}.", //
+ TYPE.get(json), this.key, json.get(this.key))
+ .ticketMitigation().toString(), exception);
+ }
+ }
+ if (defaultValue != null) {
+ return defaultValue;
+ }
+ throw new DataTypeParserException(ExaError.messageBuilder("E-VSCOMJAVA-36") //
+ .message("Datatype {{datatype}}: Missing property {{property}}.", TYPE.get(json), this.key) //
+ .ticketMitigation().toString());
+ }
+
+ @FunctionalInterface
+ interface JsonGetter {
+ T apply(JsonObject j, String s);
+ }
+
+ static class StringProperty extends DataTypeProperty {
+ StringProperty(final String key) {
+ super(key, JsonObject::getString);
+ }
+ }
+
+ static class IntProperty extends DataTypeProperty {
+ IntProperty(final String key) {
+ super(key, JsonObject::getInt);
+ }
+ }
+
+ static class CharsetProperty extends DataTypeProperty {
+ CharsetProperty(final String key) {
+ super(key, CharsetProperty::getCharset);
+ }
+
+ private static ExaCharset getCharset(final JsonObject json, final String key) {
+ switch (json.getString(key)) {
+ case "ASCII":
+ return ExaCharset.ASCII;
+ case "UTF8":
+ return ExaCharset.UTF8;
+ default:
+ throw new DataTypeParserException(ExaError.messageBuilder("E-VSCOMJAVA-38") //
+ .message("Datatype {{datatype}}: Unsupported charset {{charset}}.", TYPE.get(json),
+ json.getString(key)) //
+ .ticketMitigation().toString());
+ }
+ }
+ }
+
+ static class BooleanProperty extends DataTypeProperty {
+ BooleanProperty(final String key) {
+ super(key, JsonObject::getBoolean);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/exasol/adapter/request/parser/RequestParser.java b/src/main/java/com/exasol/adapter/request/parser/RequestParser.java
index dc9acbe..e068a93 100644
--- a/src/main/java/com/exasol/adapter/request/parser/RequestParser.java
+++ b/src/main/java/com/exasol/adapter/request/parser/RequestParser.java
@@ -6,8 +6,7 @@
import java.util.logging.Logger;
import java.util.stream.Collectors;
-import com.exasol.adapter.metadata.SchemaMetadataInfo;
-import com.exasol.adapter.metadata.TableMetadata;
+import com.exasol.adapter.metadata.*;
import com.exasol.adapter.request.*;
import com.exasol.adapter.sql.SqlStatement;
import com.exasol.errorreporting.ExaError;
@@ -74,7 +73,15 @@ private AbstractAdapterRequest parseRefreshRequest(final JsonObject root, final
private AbstractAdapterRequest parsePushdownRequest(final JsonObject root, final SchemaMetadataInfo metadataInfo) {
final SqlStatement statement = parsePushdownStatement(root);
final List involvedTables = parseInvolvedTables(root);
- return new PushDownRequest(metadataInfo, statement, involvedTables);
+ final List dataTypes = parseDataTypes(root);
+ return new PushDownRequest(metadataInfo, statement, involvedTables, dataTypes);
+ }
+
+ private List parseDataTypes(final JsonObject root) {
+ if (!root.containsKey(SELECT_LIST_DATATYPES_KEY)) {
+ return Collections.emptyList();
+ }
+ return DataTypeParser.create().parse(root.getJsonArray(SELECT_LIST_DATATYPES_KEY));
}
private List parseInvolvedTables(final JsonObject root) {
diff --git a/src/main/java/com/exasol/adapter/request/parser/RequestParserConstants.java b/src/main/java/com/exasol/adapter/request/parser/RequestParserConstants.java
index 58dda71..845290a 100644
--- a/src/main/java/com/exasol/adapter/request/parser/RequestParserConstants.java
+++ b/src/main/java/com/exasol/adapter/request/parser/RequestParserConstants.java
@@ -14,6 +14,7 @@ final class RequestParserConstants {
static final String PUSHDOW_REQUEST_KEY = "pushdownRequest";
static final String SCHEMA_METADATA_INFO_KEY = "schemaMetadataInfo";
static final String INVOLVED_TABLES_KEY = "involvedTables";
+ static final String SELECT_LIST_DATATYPES_KEY = "selectListDataTypes";
static final String DATA_TYPE = "dataType";
static final String TABLE_NAME_KEY = "name";
static final String TABLE_COMMENT_KEY = "comment";
diff --git a/src/test/java/com/exasol/adapter/AdapterCallExecutorTest.java b/src/test/java/com/exasol/adapter/AdapterCallExecutorTest.java
index d65cc33..6a4baf9 100644
--- a/src/test/java/com/exasol/adapter/AdapterCallExecutorTest.java
+++ b/src/test/java/com/exasol/adapter/AdapterCallExecutorTest.java
@@ -87,7 +87,7 @@ void testExecutePushDownRequest() throws AdapterException {
final PushDownResponse expectedResponse = PushDownResponse.builder().pushDownSql("SELECT * FROM FOOBAR")
.build();
when(this.mockAdapter.pushdown(any(), any())).thenReturn(expectedResponse);
- final String response = this.adapterCallExecutor.executeAdapterCall(new PushDownRequest(null, null, null),
+ final String response = this.adapterCallExecutor.executeAdapterCall(new PushDownRequest(null, null, null, null),
null);
assertEquals("{\"type\":\"pushdown\",\"sql\":\"SELECT * FROM FOOBAR\"}", response);
verify(this.mockAdapter).pushdown(any(), any(PushDownRequest.class));
diff --git a/src/test/java/com/exasol/adapter/metadata/DataTypeTest.java b/src/test/java/com/exasol/adapter/metadata/DataTypeTest.java
index 379a01d..548aab2 100644
--- a/src/test/java/com/exasol/adapter/metadata/DataTypeTest.java
+++ b/src/test/java/com/exasol/adapter/metadata/DataTypeTest.java
@@ -6,7 +6,14 @@
import org.junit.jupiter.api.Test;
+import nl.jqno.equalsverifier.EqualsVerifier;
+
class DataTypeTest {
+ @Test
+ void testEqualsAndHashContract() {
+ EqualsVerifier.simple().forClass(DataType.class).verify();
+ }
+
@Test
void createDecimal() {
final DataType dataType = DataType.createDecimal(10, 2);
@@ -111,7 +118,7 @@ void testCreateMaximumSizeChar() {
void createHashtype() {
final DataType dataType = DataType.createHashtype(16);
assertAll(() -> assertThat(dataType.getByteSize(), equalTo(16)),
- () -> assertThat(dataType.toString(), equalTo("HASHTYPE(16 byte)")));
+ () -> assertThat(dataType.toString(), equalTo("HASHTYPE(16 byte)")));
}
@Test
diff --git a/src/test/java/com/exasol/adapter/request/parser/DataTypeParserTest.java b/src/test/java/com/exasol/adapter/request/parser/DataTypeParserTest.java
new file mode 100644
index 0000000..b1cf78c
--- /dev/null
+++ b/src/test/java/com/exasol/adapter/request/parser/DataTypeParserTest.java
@@ -0,0 +1,263 @@
+package com.exasol.adapter.request.parser;
+
+import static com.exasol.adapter.request.parser.json.builder.JsonEntry.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.StringReader;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import com.exasol.adapter.metadata.DataType;
+import com.exasol.adapter.metadata.DataType.ExaCharset;
+import com.exasol.adapter.request.parser.DataTypeParser.DataTypeParserException;
+import com.exasol.adapter.request.parser.json.builder.JsonEntry;
+import com.exasol.adapter.request.parser.json.builder.JsonParent;
+
+import jakarta.json.*;
+
+class DataTypeParserTest {
+
+ @ParameterizedTest
+ @ValueSource(strings = { "UNKNOWN", "abcdef" })
+ @NullSource
+ void unknownDatatype_ThrowsException(final String name) {
+ final JsonEntry data = JsonEntry.array(object(entry("type", name)));
+ final Exception e = assertThrows(DataTypeParserException.class, () -> parse(data));
+ assertThat(e.getMessage(), startsWith("E-VSCOMJAVA-37: Unsupported datatype '" + name + "'"));
+ }
+
+ @Test
+ void missingType_ThrowsException() {
+ final JsonEntry data = JsonEntry.array(object(entry("no_type", "DECIMAL")));
+ final Exception e = assertThrows(DataTypeParserException.class, () -> parse(data));
+ assertThat(e.getMessage(), startsWith("E-VSCOMJAVA-40: Unspecified datatype"));
+ }
+
+ @Test
+ void varchar() {
+ verifySingle(DataType.createVarChar(21, ExaCharset.ASCII), object( //
+ entry("type", "VARCHAR"), //
+ entry("size", 21), //
+ entry("characterSet", "ASCII")));
+ }
+
+ @Test
+ void testChar() {
+ verifySingle(DataType.createChar(22, ExaCharset.UTF8), object( //
+ entry("type", "CHAR"), //
+ entry("size", 22), //
+ entry("characterSet", "UTF8")));
+ }
+
+ @Test
+ void decimal() {
+ verifySingle(DataType.createDecimal(31, 41), object( //
+ entry("type", "DECIMAL"), //
+ entry("precision", 31), //
+ entry("scale", 41)));
+ }
+
+ @Test
+ void testDouble() {
+ verifySingle(DataType.createDouble(), object(entry("type", "DOUBLE")));
+ }
+
+ @Test
+ void date() {
+ verifySingle(DataType.createDate(), object(entry("type", "DATE")));
+ }
+
+ @Test
+ void timestamp() {
+ verifySingle(DataType.createTimestamp(true), object( //
+ entry("type", "TIMESTAMP"), //
+ entry("withLocalTimeZone", true)));
+ }
+
+ @Test
+ void testBoolean() {
+ verifySingle(DataType.createBool(), object( //
+ entry("type", "BOOLEAN")));
+ }
+
+ @Test
+ void geometry() {
+ verifySingle(DataType.createGeometry(42), object( //
+ entry("type", "GEOMETRY"), //
+ entry("scale", 42)));
+ }
+
+ @Test
+ void interval() {
+ verifySingle(DataType.createIntervalDaySecond(32, 51), object( //
+ entry("type", "INTERVAL"), //
+ entry("precision", 32), //
+ entry("fraction", 51)));
+ }
+
+ @Test
+ void hashtype() {
+ verifySingle(DataType.createHashtype(2031), object( //
+ entry("type", "HASHTYPE"), //
+ entry("byteSize", 2031)));
+ }
+
+ // ----------------------------------------------
+ // default values for data type properties
+
+ @Test
+ void defaultCharacterSet_Utf8() {
+ verifySingle(DataType.createVarChar(21, ExaCharset.UTF8), object( //
+ entry("type", "VARCHAR"), //
+ entry("size", 21)));
+ verifySingle(DataType.createChar(22, ExaCharset.UTF8), object( //
+ entry("type", "CHAR"), //
+ entry("size", 22)));
+ }
+
+ @Test
+ void timestampDefaultLocalTimezone_False() {
+ verifySingle(DataType.createTimestamp(false), object(entry("type", "TIMESTAMP")));
+ }
+
+ @Test
+ void geometryDefaultSri_0() {
+ verifySingle(DataType.createGeometry(0), object(entry("type", "GEOMETRY")));
+ }
+
+ @Test
+ void intervalDefaultPrecision_2() {
+ verifySingle(DataType.createIntervalDaySecond(2, 51), object( //
+ entry("type", "INTERVAL"), //
+ entry("fraction", 51)));
+ }
+
+ @Test
+ void intervalDefaultFraction_3() {
+ verifySingle(DataType.createIntervalDaySecond(32, 3), object( //
+ entry("type", "INTERVAL"), //
+ entry("precision", 32)));
+ }
+
+ @Test
+ void hashTypeDefaultByteSize_16() {
+ verifySingle(DataType.createHashtype(16), object(entry("type", "HASHTYPE")));
+ }
+
+ @Test
+ void missingRequiredProperty_ThrowsException() {
+ verifyMissingRequiredProperty(object(entry("type", "DECIMAL")), "DECIMAL", "");
+ verifyMissingRequiredProperty(object(entry("type", "CHAR")), "CHAR", "size");
+ verifyMissingRequiredProperty(object(entry("type", "VARCHAR")), "VARCHAR", "size");
+ }
+
+ @Test
+ void illegalPropertyValue_ThrowsException() {
+ verifyIllegalPropertyValue(object( //
+ entry("type", "VARCHAR"), //
+ entry("size", 20), //
+ entry("characterSet", "xxx")), //
+ "VARCHAR", "characterSet", "\"xxx\"");
+ verifyIllegalPropertyValue("VARCHAR", "size", "abc");
+ verifyIllegalPropertyValue("TIMESTAMP", "withLocalTimeZone", 123);
+ verifyIllegalPropertyValue("HASHTYPE", "byteSize", true);
+ verifyIllegalPropertyValue("GEOMETRY", "scale", false);
+ verifyIllegalPropertyValue("INTERVAL", "precision", "p");
+ verifyIllegalPropertyValue(object(entry("type", "INTERVAL"), //
+ entry("precision", 5), //
+ entry("fraction", true)), //
+ "INTERVAL", "fraction", "true");
+ }
+
+ @Test
+ void multipleDatatypes() {
+ final JsonParent builder = JsonEntry.array( //
+ object(entry("type", "VARCHAR"), //
+ entry("size", 21), //
+ entry("characterSet", "ASCII")), //
+ object(entry("type", "CHAR"), //
+ entry("size", 22), //
+ entry("characterSet", "UTF8")), //
+ object(entry("type", "DECIMAL"), //
+ entry("precision", 31), //
+ entry("scale", 41)), //
+ object(entry("type", "DOUBLE")), //
+ object(entry("type", "DATE")), //
+ object(entry("type", "TIMESTAMP"), //
+ entry("withLocalTimeZone", true)), //
+ object(entry("type", "BOOLEAN")), //
+ object(entry("type", "GEOMETRY"), //
+ entry("scale", 42)), //
+ object(entry("type", "INTERVAL"), //
+ entry("precision", 32), //
+ entry("fraction", 51)), //
+ object(entry("type", "HASHTYPE")));
+ final List expected = List.of( //
+ DataType.createVarChar(21, ExaCharset.ASCII), //
+ DataType.createChar(22, ExaCharset.UTF8), //
+ DataType.createDecimal(31, 41), //
+ DataType.createDouble(), //
+ DataType.createDate(), //
+ DataType.createTimestamp(true), //
+ DataType.createBool(), //
+ DataType.createGeometry(42), //
+ DataType.createIntervalDaySecond(32, 51), //
+ DataType.createHashtype(16));
+ assertThat(parse(builder), equalTo(expected));
+ }
+
+ private void verifyIllegalPropertyValue(final String datatype, final String property, final String value) {
+ verifyIllegalPropertyValue(object(entry("type", datatype), entry(property, value)), datatype, property,
+ String.valueOf("\"" + value + "\""));
+ }
+
+ private void verifyIllegalPropertyValue(final String datatype, final String property, final boolean value) {
+ verifyIllegalPropertyValue(object(entry("type", datatype), entry(property, value)), datatype, property,
+ String.valueOf(value));
+ }
+
+ private void verifyIllegalPropertyValue(final String datatype, final String property, final int value) {
+ verifyIllegalPropertyValue(object(entry("type", datatype), entry(property, value)), datatype, property,
+ String.valueOf(value));
+ }
+
+ private void verifyIllegalPropertyValue(final JsonParent builder, final String datatype, final String property,
+ final String value) {
+ final JsonEntry data = array(builder);
+ final Exception e = assertThrows(DataTypeParserException.class, () -> parse(data));
+ assertThat(e.getMessage(), startsWith("E-VSCOMJAVA-39: Datatype '" + datatype + "', property '" + property
+ + "': Illegal value " + value + "."));
+ }
+
+ // --------------------------------------------
+ // test utilities
+
+ private void verifyMissingRequiredProperty(final JsonParent builder, final String datatype, final String property) {
+ final JsonEntry data = array(builder);
+ final Exception e = assertThrows(DataTypeParserException.class, () -> parse(data));
+ assertThat(e.getMessage(),
+ startsWith("E-VSCOMJAVA-36: Datatype '" + datatype + "': Missing property '" + property));
+ }
+
+ private void verifySingle(final DataType expected, final JsonEntry builder) {
+ final DataType actual = parse(array(builder)).get(0);
+ assertThat(actual, equalTo(expected));
+ }
+
+ private List parse(final JsonEntry builder) {
+ return DataTypeParser.create().parse(readArray(builder.render()));
+ }
+
+ private JsonArray readArray(final String string) {
+ try (final JsonReader jsonReader = Json.createReader(new StringReader(string))) {
+ return jsonReader.readArray();
+ }
+ }
+}
diff --git a/src/test/java/com/exasol/adapter/request/parser/RequestParserTest.java b/src/test/java/com/exasol/adapter/request/parser/RequestParserTest.java
index a9298a6..8c6769d 100644
--- a/src/test/java/com/exasol/adapter/request/parser/RequestParserTest.java
+++ b/src/test/java/com/exasol/adapter/request/parser/RequestParserTest.java
@@ -1,5 +1,6 @@
package com.exasol.adapter.request.parser;
+import static com.exasol.adapter.request.parser.json.builder.JsonEntry.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.collection.IsMapContaining.hasEntry;
@@ -9,14 +10,18 @@
import java.util.List;
import java.util.Map;
+import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import com.exasol.adapter.metadata.DataType;
import com.exasol.adapter.metadata.TableMetadata;
import com.exasol.adapter.request.*;
+import com.exasol.adapter.request.parser.json.builder.*;
class RequestParserTest {
- private static final String SCHEMA_METADATA_INFO = "\"schemaMetadataInfo\" : { \"name\" : \"foo\" }";
+ private static final JsonKeyValue SCHEMA_METADATA_INFO = JsonEntry.entry("schemaMetadataInfo",
+ object(entry("name", "foo")));
private RequestParser parser;
@BeforeEach
@@ -25,28 +30,25 @@ void beforeEach() {
}
@Test
- void testParseThrowsExceptionIfRequestTypeUnknown() {
- final String rawRequest = "{ \"type\" : \"UNKNOWN\", \"schemaMetadataInfo\" : { \"name\" : \"foo\" } }";
- final RequestParserException exception = assertThrows(RequestParserException.class,
- () -> this.parser.parse(rawRequest));
+ void requestTypeUnknown_ThrowsException() {
+ final String rawRequest = JsonEntry.object(//
+ entry("type", "UNKNOWN"), SCHEMA_METADATA_INFO).render();
+ final Exception exception = assertThrows(RequestParserException.class, () -> this.parser.parse(rawRequest));
assertThat(exception.getMessage(), containsString("E-VSCOMJAVA-16"));
}
@Test
- void testParseSetPropertiesRequest() {
- final String rawRequest = "{" //
- + " \"type\" : \"setProperties\"," //
- + " \"properties\" :" //
- + " {" //
- + " \"A\" : \"value A\"," //
- + " \"B\" : 42," //
- + " \"PI\" : 3.14," //
- + " \"YES\" : true," //
- + " \"NO\" : false," //
- + " \"NULL_value\" : null" //
- + " }," //
- + SCHEMA_METADATA_INFO //
- + "}";
+ void setPropertiesRequest() {
+ final String rawRequest = JsonEntry.object(//
+ entry("type", "setProperties"), //
+ entry("properties", object( //
+ entry("A", "value A"), //
+ entry("B", "42"), //
+ entry("PI", 3.14), //
+ entry("YES", true), //
+ entry("NO", false), //
+ entry("NULL_value", JsonEntry.nullValue()))),
+ SCHEMA_METADATA_INFO).render();
final AdapterRequest request = this.parser.parse(rawRequest);
assertThat("Request class", request, instanceOf(SetPropertiesRequest.class));
final Map properties = ((SetPropertiesRequest) request).getProperties();
@@ -61,93 +63,93 @@ void testParseSetPropertiesRequest() {
}
@Test
- void testParseRequestWithUnsupportedPropertyType() {
- final String rawRequest = "{" //
- + " \"type\" : \"setProperties\"," //
- + " \"properties\" :" //
- + " {" //
- + " \"A\" : { \"value\" : \"some_value\"}" //
- + " }," //
- + SCHEMA_METADATA_INFO //
- + "}";
+ void unsupportedPropertyType() {
+ final String rawRequest = JsonEntry.object(//
+ entry("type", "setProperties"), //
+ entry("properties", object( //
+ entry("A", object(entry("value", "some value"))))),
+ SCHEMA_METADATA_INFO).render();
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> this.parser.parse(rawRequest));
assertThat(exception.getMessage(), containsString("E-VSCOMJAVA-7"));
}
@Test
- void testParsePushDownRequest() {
- final String rawRequest = "{" //
- + " \"type\" : \"pushdown\",\n" //
- + " \"pushdownRequest\" :\n" //
- + " {\n" //
- + " \"type\" : \"select\",\n" //
- + " \"from\" :\n" //
- + " {\n" //
- + " \"name\" : \"FOO\",\n" //
- + " \"type\" : \"table\"\n" //
- + " }\n" //
- + " },\n" //
- + " \"involvedTables\" :\n" //
- + " [\n" //
- + " {\n" //
- + " \"name\" : \"FOO\",\n" //
- + " \"columns\" :\n" //
- + " [\n" //
- + " {\n" //
- + " \"name\" : \"BAR\",\n" //
- + " \"dataType\" :\n" //
- + " {\n" //
- + " \"precision\" : 18,\n" //
- + " \"scale\" : 0,\n" //
- + " \"type\" : \"DECIMAL\"\n" //
- + " }\n" //
- + " }\n" //
- + " ]\n" //
- + " }\n" //
- + " ],\n" //
- + SCHEMA_METADATA_INFO //
- + "}";
- final AdapterRequest request = this.parser.parse(rawRequest);
+ void classicPushDownRequest() {
+ final AdapterRequest request = this.parser.parse(createPushDownRequest().render());
assertThat("Request class", request, instanceOf(PushDownRequest.class));
final List involvedTables = ((PushDownRequest) request).getInvolvedTablesMetadata();
+ final List selectListDataTypes = ((PushDownRequest) request).getSelectListDataTypes();
assertAll(() -> assertThat(request.getType(), equalTo(AdapterRequestType.PUSHDOWN)),
() -> assertThat(involvedTables, iterableWithSize(1)),
- () -> assertThat(involvedTables.get(0).getName(), equalTo("FOO")));
+ () -> assertThat(involvedTables.get(0).getName(), equalTo("FOO")),
+ () -> assertThat(selectListDataTypes, empty()));
}
@Test
- void testParseRefreshRequestWithoutTableFilter() {
- final String rawRequest = "{" //
- + " \"type\" : \"refresh\",\n" //
- + SCHEMA_METADATA_INFO //
- + "}";
+ void pushDownRequestWithSelectListDataTypes() {
+ final String rawRequest = createPushDownRequest().withChild( //
+ entry("selectListDataTypes", array( //
+ object(entry("type", "DECIMAL"), //
+ entry("precision", 9), //
+ entry("scale", 10)), //
+ object(entry("type", "DOUBLE"))))) //
+ .render();
+ final AdapterRequest request = this.parser.parse(rawRequest);
+ final List selectListDataTypes = ((PushDownRequest) request).getSelectListDataTypes();
+ assertAll(() -> assertThat(request.getType(), equalTo(AdapterRequestType.PUSHDOWN)),
+ () -> assertThat(selectListDataTypes, iterableWithSize(2)),
+ () -> assertThat(selectListDataTypes.get(0), equalTo(DataType.createDecimal(9, 10))));
+ }
+
+ @Test
+ void refreshRequestWithoutTableFilter() {
+ final String rawRequest = JsonEntry.object(//
+ entry("type", "refresh"), //
+ SCHEMA_METADATA_INFO) //
+ .render();
final RefreshRequest request = (RefreshRequest) this.parser.parse(rawRequest);
assertAll(() -> assertThat(request.refreshesOnlySelectedTables(), equalTo(false)),
- () -> assertThat(request.getTables(), nullValue()));
+ () -> assertThat(request.getTables(), Matchers.nullValue()));
}
@Test
- void testParseRefreshRequestWithTableFilter() {
- final String rawRequest = "{" //
- + " \"type\" : \"refresh\",\n" //
- + " \"requestedTables\" :\n" //
- + " [" //
- + " \"T1\", \"T2\"\n" //
- + " ],\n" //
- + SCHEMA_METADATA_INFO //
- + "}";
+ void refreshRequestWithTableFilter() {
+ final String rawRequest = JsonEntry.object(//
+ entry("type", "refresh"), //
+ entry("requestedTables", array(value("T1"), value("T2"))), //
+ SCHEMA_METADATA_INFO) //
+ .render();
final RefreshRequest request = (RefreshRequest) this.parser.parse(rawRequest);
assertAll(() -> assertThat(request.refreshesOnlySelectedTables(), equalTo(true)),
() -> assertThat(request.getTables(), containsInAnyOrder("T1", "T2")));
}
@Test
- void testParseRequestWithoutSchemaMetadata() {
- final String rawRequest = "{" //
- + " \"type\" : \"refresh\"\n" //
- + "}";
+ void requestWithoutSchemaMetadata() {
+ final String rawRequest = JsonEntry.object(entry("type", "refresh")).render();
final AdapterRequest request = this.parser.parse(rawRequest);
assertThat(request.getVirtualSchemaName(), equalTo("UNKNOWN"));
}
+
+ private JsonParent createPushDownRequest() {
+ return JsonEntry.object( //
+ entry("type", "pushdown"), //
+ entry("pushdownRequest", object( //
+ entry("type", "select"), //
+ entry("from", object( //
+ entry("name", "FOO"), //
+ entry("type", "table") //
+ )))), //
+ entry("involvedTables", array(object( //
+ entry("name", "FOO"), //
+ entry("columns", array(object( //
+ entry("name", "BAR"), //
+ entry("dataType", object( //
+ entry("precision", 18), //
+ entry("scale", 0), //
+ entry("type", "DECIMAL") //
+ )))))))), //
+ SCHEMA_METADATA_INFO);
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonEntry.java b/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonEntry.java
new file mode 100644
index 0000000..8f6dba2
--- /dev/null
+++ b/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonEntry.java
@@ -0,0 +1,55 @@
+package com.exasol.adapter.request.parser.json.builder;
+
+import com.exasol.adapter.request.parser.json.builder.JsonKeyValue.Complex;
+import com.exasol.adapter.request.parser.json.builder.JsonKeyValue.Simple;
+import com.exasol.adapter.request.parser.json.builder.JsonParent.JsonArray;
+import com.exasol.adapter.request.parser.json.builder.JsonParent.JsonObject;
+
+public interface JsonEntry {
+
+ public static JsonObject object(final JsonEntry... children) {
+ return new JsonObject(children);
+ }
+
+ public static JsonArray array(final JsonEntry... children) {
+ return new JsonArray(children);
+ }
+
+ public static JsonKeyValue entry(final String key, final String value) {
+ return new Simple(key, value, "\"");
+ }
+
+ public static JsonKeyValue entry(final String key, final JsonValue value) {
+ return new Simple(key, value.render(), "");
+ }
+
+ public static JsonKeyValue entry(final String key, final double value) {
+ return new Simple(key, value);
+ }
+
+ public static JsonKeyValue entry(final String key, final int value) {
+ return new Simple(key, value);
+ }
+
+ public static JsonKeyValue entry(final String key, final boolean value) {
+ return new Simple(key, value);
+ }
+
+ public static JsonKeyValue entry(final String key, final JsonParent value) {
+ return new Complex(key, value);
+ }
+
+ public static JsonValue value(final String value) {
+ return new JsonValue(value, "\"");
+ }
+
+ public static JsonValue nullValue() {
+ return new JsonValue("null", "");
+ }
+
+ String render(int indent);
+
+ default String render() {
+ return render(0);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonKeyValue.java b/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonKeyValue.java
new file mode 100644
index 0000000..bad74e2
--- /dev/null
+++ b/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonKeyValue.java
@@ -0,0 +1,48 @@
+package com.exasol.adapter.request.parser.json.builder;
+
+public abstract class JsonKeyValue implements JsonEntry {
+
+ private final String key;
+
+ JsonKeyValue(final String key) {
+ this.key = key;
+ }
+
+ public String render(final int level, final String value) {
+ return String.format("\"%s\": %s", this.key, value);
+ }
+
+ static class Simple extends JsonKeyValue {
+ protected final Object value;
+ final String quote;
+
+ Simple(final String key, final Object value) {
+ this(key, value, "");
+ }
+
+ Simple(final String key, final Object value, final String quote) {
+ super(key);
+ this.value = value;
+ this.quote = quote;
+ }
+
+ @Override
+ public String render(final int level) {
+ return render(level, this.quote + String.valueOf(this.value) + this.quote);
+ }
+ }
+
+ static class Complex extends JsonKeyValue {
+ private final JsonEntry value;
+
+ Complex(final String key, final JsonEntry value) {
+ super(key);
+ this.value = value;
+ }
+
+ @Override
+ public String render(final int level) {
+ return render(level, this.value.render(level));
+ }
+ }
+}
diff --git a/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonParent.java b/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonParent.java
new file mode 100644
index 0000000..0a1d5d8
--- /dev/null
+++ b/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonParent.java
@@ -0,0 +1,59 @@
+package com.exasol.adapter.request.parser.json.builder;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public abstract class JsonParent implements JsonEntry {
+
+ private final String prefix;
+ private final String suffix;
+ private final List children;
+
+ JsonParent(final String prefix, final String suffix, final JsonEntry... children) {
+ this.prefix = prefix;
+ this.suffix = suffix;
+ this.children = new ArrayList<>(Arrays.asList(children));
+ }
+
+ private String renderChildren(final int level) {
+ return this.children.stream() //
+ .map(c -> c.render(level)) //
+ .collect(Collectors.joining(",\n" + indent(level), indent(level), ""));
+ }
+
+ public JsonParent withChild(final JsonEntry child) {
+ this.children.add(child);
+ return this;
+ }
+
+ @Override
+ public String render(final int level) {
+ return this.prefix + "\n" //
+ + renderChildren(level + 1) + "\n" //
+ + indent(level) + this.suffix;
+ }
+
+ static String indent(final int amount) {
+ return (amount < 1) ? "" : repeat(" ", amount * 2);
+ }
+
+ static String repeat(final String s, final int repetitions) {
+ final StringBuilder builder = new StringBuilder(repetitions);
+ for (int i = 0; i < repetitions; i++) {
+ builder.append(s);
+ }
+ return builder.toString();
+ }
+
+ public static class JsonObject extends JsonParent {
+ public JsonObject(final JsonEntry... values) {
+ super("{", "}", values);
+ }
+ }
+
+ public static class JsonArray extends JsonParent {
+ public JsonArray(final JsonEntry... values) {
+ super("[", "]", values);
+ }
+ }
+}
diff --git a/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonValue.java b/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonValue.java
new file mode 100644
index 0000000..08658d9
--- /dev/null
+++ b/src/test/java/com/exasol/adapter/request/parser/json/builder/JsonValue.java
@@ -0,0 +1,17 @@
+package com.exasol.adapter.request.parser.json.builder;
+
+public class JsonValue implements JsonEntry {
+
+ private final Object value;
+ private final String quote;
+
+ JsonValue(final Object value, final String quote) {
+ this.value = value;
+ this.quote = quote;
+ }
+
+ @Override
+ public String render(final int indent) {
+ return this.quote + String.valueOf(this.value) + this.quote;
+ }
+}
diff --git a/src/test/java/com/exasol/adapter/sql/SqlFunctionScalarCastTest.java b/src/test/java/com/exasol/adapter/sql/SqlFunctionScalarCastTest.java
index 00f8e09..bcbb5b6 100644
--- a/src/test/java/com/exasol/adapter/sql/SqlFunctionScalarCastTest.java
+++ b/src/test/java/com/exasol/adapter/sql/SqlFunctionScalarCastTest.java
@@ -2,7 +2,6 @@
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
diff --git a/src/test/java/com/exasol/adapter/sql/SqlLiteralIntervalTest.java b/src/test/java/com/exasol/adapter/sql/SqlLiteralIntervalTest.java
index 2161833..45d05a9 100644
--- a/src/test/java/com/exasol/adapter/sql/SqlLiteralIntervalTest.java
+++ b/src/test/java/com/exasol/adapter/sql/SqlLiteralIntervalTest.java
@@ -19,7 +19,6 @@ class SqlLiteralIntervalTest {
@BeforeEach
void setUp() {
this.dayToSecond = DataType.createIntervalDaySecond(1, 2);
- final DataType yearToMonth = DataType.createIntervalYearMonth(3);
this.sqlLiteralIntervalDayToSecond = new SqlLiteralInterval(VALUE, this.dayToSecond);
}