diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0f73598 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +dependencies.md linguist-generated=true +doc/changes/changelog.md linguist-generated=true +pk_generated_parent.pom linguist-generated=true +.github/workflows/broken_links_checker.yml linguist-generated=true +.github/workflows/ci-build-next-java.yml linguist-generated=true +.github/workflows/ci-build.yml linguist-generated=true +.github/workflows/dependencies_check.yml linguist-generated=true +.github/workflows/release_droid_prepare_original_checksum.yml linguist-generated=true +.github/workflows/release_droid_print_quick_checksum.yml linguist-generated=true +.github/workflows/release_droid_release_on_maven_central.yml linguist-generated=true +.github/workflows/release_droid_upload_github_release_assets.yml linguist-generated=true diff --git a/.github/workflows/release_droid_upload_github_release_assets.yml b/.github/workflows/release_droid_upload_github_release_assets.yml index 1fd0b60..6513c7b 100644 --- a/.github/workflows/release_droid_upload_github_release_assets.yml +++ b/.github/workflows/release_droid_upload_github_release_assets.yml @@ -24,7 +24,9 @@ jobs: - name: Build with Maven skipping tests run: mvn --batch-mode clean verify -DskipTests - name: Generate sha256sum files - run: find target -maxdepth 1 -name *.jar -exec bash -c 'sha256sum {} > {}.sha256' \; + run: | + cd target + find . -maxdepth 1 -name *.jar -exec bash -c 'sha256sum {} > {}.sha256' \; - name: Upload assets to the GitHub release draft uses: shogo82148/actions-upload-release-asset@v1 with: @@ -39,4 +41,4 @@ jobs: uses: shogo82148/actions-upload-release-asset@v1 with: upload_url: ${{ github.event.inputs.upload_url }} - asset_path: target/error_code_report.json \ No newline at end of file + asset_path: target/error_code_report.json diff --git a/.gitignore b/.gitignore index e69ff1e..cfd95ff 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,6 @@ venv/ *.orig *.old *.md.html -*.flattened-pom.xml \ No newline at end of file +*.flattened-pom.xml +/.apt_generated/ +/.apt_generated_tests/ diff --git a/README.md b/README.md index c7264f2..b9babfc 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,6 @@ This is one of the modules of Virtual Schemas Adapters. The libraries provided b A Virtual Schema adapter is basically a [UDF](https://docs.exasol.com/database_concepts/udf_scripts.htm). The Exasol core database communicates with this UDF using JSON strings. There are different types of messages, that define the API for a virtual Schema adapter ([protocol reference](doc/development/api/virtual_schema_api.md)). This repository wraps this JSON API with a Java API to facilitate the implementation of Virtual Schema adapters in Java. -Please note that the artifact name changed from "virtualschema-common" to "virtual-schema-common-java". First to unify the naming schemes, second to make sure the new adapters do not accidentally use the old line of libraries. - ## Information for Users * [Changelog](doc/changes/changelog.md) diff --git a/dependencies.md b/dependencies.md index 447ea66..897198c 100644 --- a/dependencies.md +++ b/dependencies.md @@ -35,22 +35,22 @@ | [Apache Maven Enforcer Plugin][21] | [Apache License, Version 2.0][17] | | [Maven Flatten Plugin][22] | [Apache Software Licenese][9] | | [org.sonatype.ossindex.maven:ossindex-maven-plugin][23] | [ASL2][9] | -| [Reproducible Build Maven Plugin][24] | [Apache 2.0][9] | -| [Maven Surefire Plugin][25] | [Apache License, Version 2.0][17] | -| [Versions Maven Plugin][26] | [Apache License, Version 2.0][17] | -| [Apache Maven Deploy Plugin][27] | [Apache License, Version 2.0][17] | -| [Apache Maven GPG Plugin][28] | [Apache License, Version 2.0][17] | -| [Apache Maven Source Plugin][29] | [Apache License, Version 2.0][17] | -| [Apache Maven Javadoc Plugin][30] | [Apache License, Version 2.0][17] | -| [Nexus Staging Maven Plugin][31] | [Eclipse Public License][32] | -| [JaCoCo :: Maven Plugin][33] | [Eclipse Public License 2.0][34] | -| [error-code-crawler-maven-plugin][35] | [MIT][4] | -| [Project keeper maven plugin][36] | [The MIT License][37] | -| [Maven Clean Plugin][38] | [The Apache Software License, Version 2.0][9] | -| [Maven Resources Plugin][39] | [The Apache Software License, Version 2.0][9] | -| [Maven JAR Plugin][40] | [The Apache Software License, Version 2.0][9] | -| [Maven Install Plugin][41] | [The Apache Software License, Version 2.0][9] | -| [Maven Site Plugin 3][42] | [The Apache Software License, Version 2.0][9] | +| [Maven Surefire Plugin][24] | [Apache License, Version 2.0][17] | +| [Versions Maven Plugin][25] | [Apache License, Version 2.0][17] | +| [Apache Maven Deploy Plugin][26] | [Apache License, Version 2.0][17] | +| [Apache Maven GPG Plugin][27] | [Apache License, Version 2.0][17] | +| [Apache Maven Source Plugin][28] | [Apache License, Version 2.0][17] | +| [Apache Maven Javadoc Plugin][29] | [Apache License, Version 2.0][17] | +| [Nexus Staging Maven Plugin][30] | [Eclipse Public License][31] | +| [JaCoCo :: Maven Plugin][32] | [Eclipse Public License 2.0][33] | +| [error-code-crawler-maven-plugin][34] | [MIT License][35] | +| [Reproducible Build Maven Plugin][36] | [Apache 2.0][9] | +| [Project keeper maven plugin][37] | [The MIT License][38] | +| [Maven Clean Plugin][39] | [The Apache Software License, Version 2.0][9] | +| [Maven Resources Plugin][40] | [The Apache Software License, Version 2.0][9] | +| [Maven JAR Plugin][41] | [The Apache Software License, Version 2.0][9] | +| [Maven Install Plugin][42] | [The Apache Software License, Version 2.0][9] | +| [Maven Site Plugin 3][43] | [The Apache Software License, Version 2.0][9] | [0]: https://github.com/eclipse-ee4j/jsonp [1]: https://projects.eclipse.org/license/epl-2.0 @@ -76,22 +76,23 @@ [21]: https://maven.apache.org/enforcer/maven-enforcer-plugin/ [22]: https://www.mojohaus.org/flatten-maven-plugin/ [23]: https://sonatype.github.io/ossindex-maven/maven-plugin/ -[24]: http://zlika.github.io/reproducible-build-maven-plugin -[25]: https://maven.apache.org/surefire/maven-surefire-plugin/ -[26]: http://www.mojohaus.org/versions-maven-plugin/ -[27]: https://maven.apache.org/plugins/maven-deploy-plugin/ -[28]: https://maven.apache.org/plugins/maven-gpg-plugin/ -[29]: https://maven.apache.org/plugins/maven-source-plugin/ -[30]: https://maven.apache.org/plugins/maven-javadoc-plugin/ -[31]: http://www.sonatype.com/public-parent/nexus-maven-plugins/nexus-staging/nexus-staging-maven-plugin/ -[32]: http://www.eclipse.org/legal/epl-v10.html -[33]: https://www.jacoco.org/jacoco/trunk/doc/maven.html -[34]: https://www.eclipse.org/legal/epl-2.0/ -[35]: https://github.com/exasol/error-code-crawler-maven-plugin -[36]: https://github.com/exasol/project-keeper/ -[37]: https://github.com/exasol/project-keeper/blob/main/LICENSE -[38]: http://maven.apache.org/plugins/maven-clean-plugin/ -[39]: http://maven.apache.org/plugins/maven-resources-plugin/ -[40]: http://maven.apache.org/plugins/maven-jar-plugin/ -[41]: http://maven.apache.org/plugins/maven-install-plugin/ -[42]: http://maven.apache.org/plugins/maven-site-plugin/ +[24]: https://maven.apache.org/surefire/maven-surefire-plugin/ +[25]: http://www.mojohaus.org/versions-maven-plugin/ +[26]: https://maven.apache.org/plugins/maven-deploy-plugin/ +[27]: https://maven.apache.org/plugins/maven-gpg-plugin/ +[28]: https://maven.apache.org/plugins/maven-source-plugin/ +[29]: https://maven.apache.org/plugins/maven-javadoc-plugin/ +[30]: http://www.sonatype.com/public-parent/nexus-maven-plugins/nexus-staging/nexus-staging-maven-plugin/ +[31]: http://www.eclipse.org/legal/epl-v10.html +[32]: https://www.jacoco.org/jacoco/trunk/doc/maven.html +[33]: https://www.eclipse.org/legal/epl-2.0/ +[34]: https://github.com/exasol/error-code-crawler-maven-plugin/ +[35]: https://github.com/exasol/error-code-crawler-maven-plugin/blob/main/LICENSE +[36]: http://zlika.github.io/reproducible-build-maven-plugin +[37]: https://github.com/exasol/project-keeper/ +[38]: https://github.com/exasol/project-keeper/blob/main/LICENSE +[39]: http://maven.apache.org/plugins/maven-clean-plugin/ +[40]: http://maven.apache.org/plugins/maven-resources-plugin/ +[41]: http://maven.apache.org/plugins/maven-jar-plugin/ +[42]: http://maven.apache.org/plugins/maven-install-plugin/ +[43]: http://maven.apache.org/plugins/maven-site-plugin/ diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md index 4dd411c..1b6b5ca 100644 --- a/doc/changes/changelog.md +++ b/doc/changes/changelog.md @@ -1,5 +1,6 @@ # Changes +* [16.0.0](changes_16.0.0.md) * [15.3.3](changes_15.3.3.md) * [15.3.2](changes_15.3.2.md) * [15.3.1](changes_15.3.1.md) diff --git a/doc/changes/changes_16.0.0.md b/doc/changes/changes_16.0.0.md new file mode 100644 index 0000000..0bcb211 --- /dev/null +++ b/doc/changes/changes_16.0.0.md @@ -0,0 +1,25 @@ +# Common module of Exasol Virtual Schemas Adapters 16.0.0, released 2022-??-?? + +Code name: Evaluate expected resultset datatypes + +## Summary + +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. + +This create a list of benefits +* Improved performance of queries to virtual schema by avoiding one query for each push down +* Enhanced accuracy of data type mapping +* Simplified data type mapping which is easier to extend +* Support for additional use cases + +## Features + +* #249: Evaluate expected resultset datatypes + +## Dependency Updates + +### Plugin Dependency Updates + +* Updated `com.exasol:error-code-crawler-maven-plugin:1.1.1` to `1.1.2` +* Updated `com.exasol:project-keeper-maven-plugin:2.5.0` to `2.7.0` +* Updated `org.apache.maven.plugins:maven-enforcer-plugin:3.0.0` to `3.1.0` diff --git a/error_code_config.yml b/error_code_config.yml index 976a81b..4cf6b08 100644 --- a/error_code_config.yml +++ b/error_code_config.yml @@ -2,4 +2,4 @@ error-tags: VSCOMJAVA: packages: - com.exasol - highest-index: 35 \ No newline at end of file + highest-index: 40 \ No newline at end of file diff --git a/pk_generated_parent.pom b/pk_generated_parent.pom index 129bc1a..23ceca7 100644 --- a/pk_generated_parent.pom +++ b/pk_generated_parent.pom @@ -3,7 +3,7 @@ 4.0.0 com.exasol virtual-schema-common-java-generated-parent - 15.3.3 + 16.0.0 pom UTF-8 @@ -52,7 +52,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0 + 3.1.0 enforce-maven @@ -108,20 +108,6 @@ - - io.github.zlika - reproducible-build-maven-plugin - 0.15 - - - strip-jar - package - - strip-jar - - - - org.apache.maven.plugins maven-surefire-plugin @@ -275,7 +261,7 @@ com.exasol error-code-crawler-maven-plugin - 1.1.1 + 1.1.2 verify @@ -285,6 +271,20 @@ + + io.github.zlika + reproducible-build-maven-plugin + 0.15 + + + strip-jar + package + + strip-jar + + + + diff --git a/pom.xml b/pom.xml index 922cd3f..05e962e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,9 +1,8 @@ 4.0.0 - com.exasol virtual-schema-common-java - 15.3.3 + 16.0.0 Common module of Exasol Virtual Schemas Adapters This is one of the modules of Virtual Schemas Adapters. The libraries provided by this project are the foundation of the adapter development, i.e. adapters must be implemented on top of them. @@ -89,7 +88,7 @@ com.exasol project-keeper-maven-plugin - 2.5.0 + 2.7.0 @@ -103,7 +102,7 @@ virtual-schema-common-java-generated-parent com.exasol - 15.3.3 + 16.0.0 pk_generated_parent.pom diff --git a/src/main/java/com/exasol/adapter/request/PushDownRequest.java b/src/main/java/com/exasol/adapter/request/PushDownRequest.java index 3ae9504..2265713 100644 --- a/src/main/java/com/exasol/adapter/request/PushDownRequest.java +++ b/src/main/java/com/exasol/adapter/request/PushDownRequest.java @@ -5,34 +5,36 @@ import com.exasol.ExaMetadata; import com.exasol.adapter.AdapterCallExecutor; import com.exasol.adapter.AdapterException; -import com.exasol.adapter.metadata.SchemaMetadataInfo; -import com.exasol.adapter.metadata.TableMetadata; +import com.exasol.adapter.metadata.*; import com.exasol.adapter.sql.SqlStatement; /** * This class represents a request that tells a Virtual Schema Adapter to push a SQL statement down to the external data - * source + * source. */ public class PushDownRequest extends AbstractAdapterRequest { private final SqlStatement select; private final List involvedTablesMetadata; + private final List selectListDataTypes; /** - * Create a new request of type {@link PushDownRequest} + * Create a new request of type {@link PushDownRequest}. * * @param schemaMetadataInfo schema metadata * @param select SQL statement to be pushed down to the external data source * @param involvedTablesMetadata tables involved in the push-down request + * @param selectListDataType expected data types for the result set */ public PushDownRequest(final SchemaMetadataInfo schemaMetadataInfo, final SqlStatement select, - final List involvedTablesMetadata) { + final List involvedTablesMetadata, final List selectListDataType) { super(schemaMetadataInfo, AdapterRequestType.PUSHDOWN); this.select = select; this.involvedTablesMetadata = involvedTablesMetadata; + this.selectListDataTypes = selectListDataType; } /** - * Get the SELECT statement that should be pushed down to the external data source + * Get the SELECT statement that should be pushed down to the external data source. * * @return SELECT statement */ @@ -49,6 +51,15 @@ public List getInvolvedTablesMetadata() { return this.involvedTablesMetadata; } + /** + * Get the expected data types for the result set. + * + * @return expected data types for the result set + */ + public List getSelectListDataTypes() { + return this.selectListDataTypes; + } + @Override public String executeWith(final AdapterCallExecutor adapterCallExecutor, final ExaMetadata metadata) throws AdapterException { diff --git a/src/main/java/com/exasol/adapter/request/parser/AbstractRequestParser.java b/src/main/java/com/exasol/adapter/request/parser/AbstractRequestParser.java index 0c47321..a882f75 100644 --- a/src/main/java/com/exasol/adapter/request/parser/AbstractRequestParser.java +++ b/src/main/java/com/exasol/adapter/request/parser/AbstractRequestParser.java @@ -6,7 +6,6 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.Map.Entry; -import java.util.logging.Logger; import com.exasol.errorreporting.ExaError; @@ -17,7 +16,6 @@ * Abstract base class for parsers reading fragments of the Virtual Schema requests. */ class AbstractRequestParser { - private static final Logger LOGGER = Logger.getLogger(AbstractRequestParser.class.getName()); /** * Create a JSON reader for raw request data. @@ -36,7 +34,7 @@ protected JsonReader createJsonReader(final String rawRequest) { /** * Read the properties from the schema metadata. - * + * * @param jsonSchemaMetadataInfo json schema metadata info * @return parsed Properties. */ @@ -85,7 +83,6 @@ private void addProperty(final Map properties, final Entry "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); }