diff --git a/doc/changes/changes_18.0.2.md b/doc/changes/changes_18.0.2.md index edbd4893..e7ed6481 100644 --- a/doc/changes/changes_18.0.2.md +++ b/doc/changes/changes_18.0.2.md @@ -25,6 +25,8 @@ Code name: Improve code quality * #308: Fixed `SqlLimit` so `limit` and `offset` stay non-negative for the full object lifetime by making the node immutable and aligning the validation message with the accepted zero values. * #307: Fixed `PushdownSqlRenderer` so `HASHTYPE` data types include `bytesize` in rendered pushdown SQL JSON. * #312: Fixed logging utility resource and handler lifecycle issues by closing version metadata streams, falling back when the thread context class loader is missing, defaulting absent version properties to `UNKNOWN`, closing replaced root log handlers, and resetting closed remote socket handlers before reuse. +* #318: Fixed several small naming, Javadoc, message, and parsing robustness issues, including locale-independent SQL parser uppercasing and trimmed telemetry property parsing. +* Fixed `ColumnMetadata.toString()` so `defaultValue` quotes stay balanced even when no comment is set. * #314: Fixed `SqlFunctionAggregateListagg.Behavior` so LISTAGG overflow behavior is null-safe, value-based, and constructed immutably by the parser. ## Dependency Updates diff --git a/src/main/java/com/exasol/adapter/AdapterContext.java b/src/main/java/com/exasol/adapter/AdapterContext.java index b1059868..4218a54c 100644 --- a/src/main/java/com/exasol/adapter/AdapterContext.java +++ b/src/main/java/com/exasol/adapter/AdapterContext.java @@ -13,7 +13,7 @@ public class AdapterContext { /** * Create a new AdapterContext. * - * @param telemetryClient telemetry client to be used by the adapter for reportin feature usage. + * @param telemetryClient telemetry client to be used by the adapter for reporting feature usage. */ public AdapterContext(final TelemetryClient telemetryClient) { this.telemetryClient = telemetryClient; @@ -26,7 +26,7 @@ public AdapterContext(final TelemetryClient telemetryClient) { * * * @return telemetry client diff --git a/src/main/java/com/exasol/adapter/metadata/ColumnMetadata.java b/src/main/java/com/exasol/adapter/metadata/ColumnMetadata.java index 43dec6a0..a1dba2f5 100644 --- a/src/main/java/com/exasol/adapter/metadata/ColumnMetadata.java +++ b/src/main/java/com/exasol/adapter/metadata/ColumnMetadata.java @@ -136,9 +136,10 @@ public String toString() { if (this.hasDefault()) { builder.append(", defaultValue=\""); builder.append(this.defaultValue); + builder.append("\""); } if (this.hasComment()) { - builder.append("\", comment=\""); + builder.append(", comment=\""); builder.append(this.comment); builder.append("\""); } diff --git a/src/main/java/com/exasol/adapter/request/AdapterTelemetryConfiguration.java b/src/main/java/com/exasol/adapter/request/AdapterTelemetryConfiguration.java index c6d9b626..73a9eec1 100644 --- a/src/main/java/com/exasol/adapter/request/AdapterTelemetryConfiguration.java +++ b/src/main/java/com/exasol/adapter/request/AdapterTelemetryConfiguration.java @@ -26,7 +26,8 @@ private AdapterTelemetryConfiguration(final boolean telemetryDisabled) { */ public static AdapterTelemetryConfiguration parseFromProperties(final Map properties) { final String telemetryPropertyValue = properties.get(TELEMETRY_PROPERTY); - final boolean telemetryDisabled = telemetryPropertyValue != null && !Boolean.parseBoolean(telemetryPropertyValue); + final boolean telemetryDisabled = telemetryPropertyValue != null + && !Boolean.parseBoolean(telemetryPropertyValue.trim()); return new AdapterTelemetryConfiguration(telemetryDisabled); } diff --git a/src/main/java/com/exasol/adapter/request/parser/PushdownSqlParser.java b/src/main/java/com/exasol/adapter/request/parser/PushdownSqlParser.java index d9d00b9d..8fb2f6bd 100644 --- a/src/main/java/com/exasol/adapter/request/parser/PushdownSqlParser.java +++ b/src/main/java/com/exasol/adapter/request/parser/PushdownSqlParser.java @@ -144,7 +144,7 @@ private SqlGroupBy parseGroupBy( private boolean hasAggregateFunction(final List nodesList) { // Stack is less efficient than ArrayDeque, but ArrayDeque doesn't support null elements. @java.lang.SuppressWarnings("java:S1149") - Stack expressions = new Stack<>(); + final Stack expressions = new Stack<>(); expressions.addAll(nodesList); while (!expressions.isEmpty()) { final SqlNode expression = expressions.pop(); @@ -199,82 +199,82 @@ public SqlNode parseExpression(final JsonObject expression) { final String typeName = expression.getString(TYPE, ""); final SqlNodeType type = fromTypeName(typeName); switch (type) { - case SELECT: - return parseSelect(expression); - case TABLE: - return parseTable(expression); - case JOIN: - return parseJoin(expression); - case COLUMN: - return parseColumn(expression); - case LITERAL_NULL: - return parseLiteralNull(); - case LITERAL_BOOL: - return parseLiteralBool(expression); - case LITERAL_DATE: - return parseLiteralDate(expression); - case LITERAL_TIMESTAMP: - return parseLiteralTimestamp(expression); - case LITERAL_TIMESTAMPUTC: - return parseLiteralTimestamputc(expression); - case LITERAL_DOUBLE: - return parseLiteralDouble(expression); - case LITERAL_EXACTNUMERIC: - return parseLiteralExactNumeric(expression); - case LITERAL_STRING: - return parseLiteralString(expression); - case LITERAL_INTERVAL: - return parseLiteralInterval(expression); - case PREDICATE_AND: - return parsePredicateAnd(expression); - case PREDICATE_OR: - return parsePredicateOr(expression); - case PREDICATE_NOT: - return parsePredicateNot(expression); - case PREDICATE_EQUAL: - return parsePredicateEqual(expression); - case PREDICATE_NOTEQUAL: - return parsePredicateNotEqual(expression); - case PREDICATE_LESS: - return parsePredicateLess(expression); - case PREDICATE_LESSEQUAL: - return parsePredicateLessEqual(expression); - case PREDICATE_LIKE: - return parsePredicateLike(expression); - case PREDICATE_LIKE_REGEXP: - return parsePredicateLikeRegexp(expression); - case PREDICATE_BETWEEN: - return parsePredicateBetween(expression); - case PREDICATE_IN_CONSTLIST: - return parsePredicateInConstlist(expression); - case PREDICATE_IS_JSON: - return parsePredicateIsJson(expression); - case PREDICATE_IS_NOT_JSON: - return parsePredicateIsNotJson(expression); - case PREDICATE_IS_NULL: - return parsePredicateIsNull(expression); - case PREDICATE_IS_NOT_NULL: - return parsePredicateIsNotNull(expression); - case FUNCTION_SCALAR: - return parseFunctionScalar(expression); - case FUNCTION_SCALAR_EXTRACT: - return parseFunctionScalarExtract(expression); - case FUNCTION_SCALAR_CASE: - return parseFunctionScalarCase(expression); - case FUNCTION_SCALAR_CAST: - return parseFunctionScalarCast(expression); - case FUNCTION_SCALAR_JSON_VALUE: - return parseFunctionScalarJsonValue(expression); - case FUNCTION_AGGREGATE: - return parseFunctionAggregate(expression); - case FUNCTION_AGGREGATE_GROUP_CONCAT: - return parseFunctionAggregateGroupConcat(expression); - case FUNCTION_AGGREGATE_LISTAGG: - return parseFunctionAggregateListagg(expression); - default: - throw new IllegalArgumentException(ExaError.messageBuilder("E-VSCOMJAVA-8") // - .message("Unknown node type: {{typeName}}") // - .parameter("typeName", typeName).toString()); + case SELECT: + return parseSelect(expression); + case TABLE: + return parseTable(expression); + case JOIN: + return parseJoin(expression); + case COLUMN: + return parseColumn(expression); + case LITERAL_NULL: + return parseLiteralNull(); + case LITERAL_BOOL: + return parseLiteralBool(expression); + case LITERAL_DATE: + return parseLiteralDate(expression); + case LITERAL_TIMESTAMP: + return parseLiteralTimestamp(expression); + case LITERAL_TIMESTAMPUTC: + return parseLiteralTimestamputc(expression); + case LITERAL_DOUBLE: + return parseLiteralDouble(expression); + case LITERAL_EXACTNUMERIC: + return parseLiteralExactNumeric(expression); + case LITERAL_STRING: + return parseLiteralString(expression); + case LITERAL_INTERVAL: + return parseLiteralInterval(expression); + case PREDICATE_AND: + return parsePredicateAnd(expression); + case PREDICATE_OR: + return parsePredicateOr(expression); + case PREDICATE_NOT: + return parsePredicateNot(expression); + case PREDICATE_EQUAL: + return parsePredicateEqual(expression); + case PREDICATE_NOTEQUAL: + return parsePredicateNotEqual(expression); + case PREDICATE_LESS: + return parsePredicateLess(expression); + case PREDICATE_LESSEQUAL: + return parsePredicateLessEqual(expression); + case PREDICATE_LIKE: + return parsePredicateLike(expression); + case PREDICATE_LIKE_REGEXP: + return parsePredicateLikeRegexp(expression); + case PREDICATE_BETWEEN: + return parsePredicateBetween(expression); + case PREDICATE_IN_CONSTLIST: + return parsePredicateInConstlist(expression); + case PREDICATE_IS_JSON: + return parsePredicateIsJson(expression); + case PREDICATE_IS_NOT_JSON: + return parsePredicateIsNotJson(expression); + case PREDICATE_IS_NULL: + return parsePredicateIsNull(expression); + case PREDICATE_IS_NOT_NULL: + return parsePredicateIsNotNull(expression); + case FUNCTION_SCALAR: + return parseFunctionScalar(expression); + case FUNCTION_SCALAR_EXTRACT: + return parseFunctionScalarExtract(expression); + case FUNCTION_SCALAR_CASE: + return parseFunctionScalarCase(expression); + case FUNCTION_SCALAR_CAST: + return parseFunctionScalarCast(expression); + case FUNCTION_SCALAR_JSON_VALUE: + return parseFunctionScalarJsonValue(expression); + case FUNCTION_AGGREGATE: + return parseFunctionAggregate(expression); + case FUNCTION_AGGREGATE_GROUP_CONCAT: + return parseFunctionAggregateGroupConcat(expression); + case FUNCTION_AGGREGATE_LISTAGG: + return parseFunctionAggregateListagg(expression); + default: + throw new IllegalArgumentException(ExaError.messageBuilder("E-VSCOMJAVA-8") // + .message("Unknown node type: {{typeName}}") // + .parameter("typeName", typeName).toString()); } } @@ -463,49 +463,49 @@ private List collectInvolvedTables(final SqlNode from) { while (!nodes.isEmpty()) { final SqlNode node = nodes.pop(); switch (node.getType()) { - case TABLE: - involvedTables.add((SqlTable) node); - break; - case JOIN: - nodes.add(((SqlJoin) node).getRight()); - nodes.add(((SqlJoin) node).getLeft()); - break; - default: - throw new IllegalStateException(ExaError.messageBuilder("E-VSCOMJAVA-10") - .message("Encountered illegal SqlNodeType during collection involved tables: {{nodeType}}") - .parameter("nodeType", node.getType()).toString()); + case TABLE: + involvedTables.add((SqlTable) node); + break; + case JOIN: + nodes.add(((SqlJoin) node).getRight()); + nodes.add(((SqlJoin) node).getLeft()); + break; + default: + throw new IllegalStateException(ExaError.messageBuilder("E-VSCOMJAVA-10") + .message("Encountered illegal SqlNodeType during collection involved tables: {{nodeType}}") + .parameter("nodeType", node.getType()).toString()); } } return involvedTables; } private DataType getDataType(final JsonObject dataType) { - final String typeName = dataType.getString(TYPE).toUpperCase(); + final String typeName = dataType.getString(TYPE).toUpperCase(Locale.ROOT); switch (typeName) { - case "DECIMAL": - return DataType.createDecimal(dataType.getInt(PRECISION), dataType.getInt(SCALE)); - case "DOUBLE": - return DataType.createDouble(); - case "VARCHAR": - return getVarchar(dataType); - case "CHAR": - return getChar(dataType); - case "BOOLEAN": - return DataType.createBool(); - case "DATE": - return DataType.createDate(); - case "TIMESTAMP": - return getTimestamp(dataType); - case "INTERVAL": - return getInterval(dataType); - case "GEOMETRY": - return getGeometry(dataType); - case "HASHTYPE": - return getHashtype(dataType); - default: - throw new IllegalArgumentException(ExaError.messageBuilder("E-VSCOMJAVA-11") - .message("Unsupported data type encountered: {{typeName}}.") // - .parameter("typeName", typeName).toString()); + case "DECIMAL": + return DataType.createDecimal(dataType.getInt(PRECISION), dataType.getInt(SCALE)); + case "DOUBLE": + return DataType.createDouble(); + case "VARCHAR": + return getVarchar(dataType); + case "CHAR": + return getChar(dataType); + case "BOOLEAN": + return DataType.createBool(); + case "DATE": + return DataType.createDate(); + case "TIMESTAMP": + return getTimestamp(dataType); + case "INTERVAL": + return getInterval(dataType); + case "GEOMETRY": + return getGeometry(dataType); + case "HASHTYPE": + return getHashtype(dataType); + default: + throw new IllegalArgumentException(ExaError.messageBuilder("E-VSCOMJAVA-11") + .message("Unsupported data type encountered: {{typeName}}.") // + .parameter("typeName", typeName).toString()); } } @@ -545,7 +545,7 @@ private SqlNode parsePredicateBetween(final JsonObject exp) { private SqlNode parsePredicateIsJson(final JsonObject jsonExpression) { final SqlNode expression = parseExpression(jsonExpression.getJsonObject(EXPRESSION)); final TypeConstraints typeConstraint = TypeConstraints - .valueOf(jsonExpression.getString(TYPE_CONSTRAINT).toUpperCase()); + .valueOf(jsonExpression.getString(TYPE_CONSTRAINT).toUpperCase(Locale.ROOT)); final KeyUniquenessConstraint keyUniquenessConstraint = KeyUniquenessConstraint .of(jsonExpression.getString(KEY_UNIQUENESS_CONSTRAINT)); return new SqlPredicateIsJson(expression, typeConstraint, keyUniquenessConstraint); @@ -554,7 +554,7 @@ private SqlNode parsePredicateIsJson(final JsonObject jsonExpression) { private SqlNode parsePredicateIsNotJson(final JsonObject jsonExpression) { final SqlNode expression = parseExpression(jsonExpression.getJsonObject(EXPRESSION)); final TypeConstraints typeConstraint = TypeConstraints - .valueOf(jsonExpression.getString(TYPE_CONSTRAINT).toUpperCase()); + .valueOf(jsonExpression.getString(TYPE_CONSTRAINT).toUpperCase(Locale.ROOT)); final KeyUniquenessConstraint keyUniquenessConstraint = KeyUniquenessConstraint .of(jsonExpression.getString(KEY_UNIQUENESS_CONSTRAINT)); return new SqlPredicateIsNotJson(expression, typeConstraint, keyUniquenessConstraint); @@ -681,7 +681,8 @@ private SqlNode parseFunctionAggregateListagg(final JsonObject expression) { private Behavior parseOverflowBehavior(final JsonObject expression) { final JsonObject overflowBehaviorJson = expression.getJsonObject(OVERFLOW_BEHAVIOUR); - final BehaviorType behaviorType = BehaviorType.valueOf(overflowBehaviorJson.getString(TYPE).toUpperCase()); + final BehaviorType behaviorType = BehaviorType + .valueOf(overflowBehaviorJson.getString(TYPE).toUpperCase(Locale.ROOT)); if (behaviorType == BehaviorType.TRUNCATE) { final TruncationType truncationType = TruncationType .parseTruncationType(overflowBehaviorJson.getString(TRUNCATION_TYPE)); @@ -699,28 +700,28 @@ private Behavior parseOverflowBehavior(final JsonObject expression) { * Mapping from join type name (as in json api) to enum */ private static JoinType fromJoinTypeName(final String typeName) { - return Enum.valueOf(JoinType.class, typeName.toUpperCase()); + return Enum.valueOf(JoinType.class, typeName.toUpperCase(Locale.ROOT)); } /** * Mapping from scalar function name (as in json api) to enum */ private static ScalarFunction fromScalarFunctionName(final String functionName) { - return Enum.valueOf(ScalarFunction.class, functionName.toUpperCase()); + return Enum.valueOf(ScalarFunction.class, functionName.toUpperCase(Locale.ROOT)); } /** * Mapping from aggregate function name (as in json api) to enum */ private static AggregateFunction fromAggregationFunctionName(final String functionName) { - return Enum.valueOf(AggregateFunction.class, functionName.toUpperCase()); + return Enum.valueOf(AggregateFunction.class, functionName.toUpperCase(Locale.ROOT)); } /** * Mapping from type name (as in json api) to enum */ private static SqlNodeType fromTypeName(final String typeName) { - return Enum.valueOf(SqlNodeType.class, typeName.toUpperCase()); + return Enum.valueOf(SqlNodeType.class, typeName.toUpperCase(Locale.ROOT)); } private TableMetadata findInvolvedTableMetadata(final String tableName) { 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 29ea2ad8..85623561 100644 --- a/src/main/java/com/exasol/adapter/request/parser/RequestParser.java +++ b/src/main/java/com/exasol/adapter/request/parser/RequestParser.java @@ -84,7 +84,7 @@ private AbstractAdapterRequest parseRefreshRequest(final JsonObject root, final } private AbstractAdapterRequest parsePushdownRequest(final JsonObject root, final SchemaMetadataInfo metadataInfo) { - final JsonObject pushdownJson = root.getJsonObject(PUSHDOW_REQUEST_KEY); + final JsonObject pushdownJson = root.getJsonObject(PUSHDOWN_REQUEST_KEY); final List involvedTables = parseInvolvedTables(root); final SqlStatement statement = parsePushdownStatement(pushdownJson, involvedTables); final List dataTypes = parseDataTypes(pushdownJson); 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 845290a6..a55380f8 100644 --- a/src/main/java/com/exasol/adapter/request/parser/RequestParserConstants.java +++ b/src/main/java/com/exasol/adapter/request/parser/RequestParserConstants.java @@ -11,7 +11,7 @@ final class RequestParserConstants { static final String ADAPTER_NOTES_KEY = "adapterNotes"; static final String PROPERTIES_KEY = "properties"; static final String SCHEMA_NAME_KEY = "name"; - static final String PUSHDOW_REQUEST_KEY = "pushdownRequest"; + static final String PUSHDOWN_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"; @@ -24,4 +24,4 @@ final class RequestParserConstants { private RequestParserConstants() { // prevent instantiation } -} \ No newline at end of file +} diff --git a/src/main/java/com/exasol/adapter/request/renderer/PushdownSqlRenderer.java b/src/main/java/com/exasol/adapter/request/renderer/PushdownSqlRenderer.java index bd296bc0..ea44c6f4 100644 --- a/src/main/java/com/exasol/adapter/request/renderer/PushdownSqlRenderer.java +++ b/src/main/java/com/exasol/adapter/request/renderer/PushdownSqlRenderer.java @@ -34,7 +34,9 @@ public JsonValue render(final SqlNode node) { return node.accept(new ConvertVisitor()); } catch (final AdapterException exception) { throw new IllegalStateException(ExaError.messageBuilder("F-VSCOMJAVA-34") - .message("n unexpected error occurred during request serialization.").ticketMitigation().toString(), + .message("An unexpected error occurred during request serialization: {{error message}}.") + .parameter("error message", exception.getMessage()) + .ticketMitigation().toString(), exception); } } @@ -159,46 +161,46 @@ private JsonObject render(final DataType dataType) { final DataType.ExaDataType type = dataType.getExaDataType(); builder.add(TYPE, type.name().toUpperCase()); switch (type) { - case DECIMAL: - builder.add(PRECISION, dataType.getPrecision()); - builder.add(SCALE, dataType.getScale()); - break; - case VARCHAR: - case CHAR: - builder.add(CHARACTER_SET, dataType.getCharset().name().toLowerCase()); - builder.add(SIZE, dataType.getSize()); - break; - case TIMESTAMP: - builder.add(WITH_LOCAL_TIME_ZONE, dataType.isWithLocalTimezone()); - break; - case INTERVAL: - builder.add(PRECISION, dataType.getPrecision()); - builder.add(FROM_TO, render(dataType.getIntervalType())); - if (dataType.getIntervalType().equals(DataType.IntervalType.DAY_TO_SECOND)) { - builder.add(FRACTION, dataType.getIntervalFraction()); - } - break; - case GEOMETRY: - builder.add(SRID, dataType.getGeometrySrid()); - break; - case HASHTYPE: - builder.add(BYTE_SIZE, dataType.getByteSize()); - break; - default: - break; + case DECIMAL: + builder.add(PRECISION, dataType.getPrecision()); + builder.add(SCALE, dataType.getScale()); + break; + case VARCHAR: + case CHAR: + builder.add(CHARACTER_SET, dataType.getCharset().name().toLowerCase()); + builder.add(SIZE, dataType.getSize()); + break; + case TIMESTAMP: + builder.add(WITH_LOCAL_TIME_ZONE, dataType.isWithLocalTimezone()); + break; + case INTERVAL: + builder.add(PRECISION, dataType.getPrecision()); + builder.add(FROM_TO, render(dataType.getIntervalType())); + if (dataType.getIntervalType().equals(DataType.IntervalType.DAY_TO_SECOND)) { + builder.add(FRACTION, dataType.getIntervalFraction()); + } + break; + case GEOMETRY: + builder.add(SRID, dataType.getGeometrySrid()); + break; + case HASHTYPE: + builder.add(BYTE_SIZE, dataType.getByteSize()); + break; + default: + break; } return builder.build(); } private String render(final DataType.IntervalType intervalType) { switch (intervalType) { - case DAY_TO_SECOND: - return "DAY TO SECONDS"; - case YEAR_TO_MONTH: - return "YEAR TO MONTH"; - default: - throw new IllegalStateException(ExaError.messageBuilder("F-VSCOMJAVA-35") - .message("Unimplemented interval type {{type}}.", intervalType).ticketMitigation().toString()); + case DAY_TO_SECOND: + return "DAY TO SECONDS"; + case YEAR_TO_MONTH: + return "YEAR TO MONTH"; + default: + throw new IllegalStateException(ExaError.messageBuilder("F-VSCOMJAVA-35") + .message("Unimplemented interval type {{type}}.", intervalType).ticketMitigation().toString()); } } diff --git a/src/test/java/com/exasol/adapter/metadata/ColumnMetadataTest.java b/src/test/java/com/exasol/adapter/metadata/ColumnMetadataTest.java index ffced623..d6044476 100644 --- a/src/test/java/com/exasol/adapter/metadata/ColumnMetadataTest.java +++ b/src/test/java/com/exasol/adapter/metadata/ColumnMetadataTest.java @@ -132,6 +132,12 @@ void assertToStringWithOptionalFields() { } + @Test + void assertToStringWithDefaultValueWithoutComment() { + assertThat(this.builder.defaultValue("default").build().toString(), + equalTo("ColumnMetadata{name=\"COLUMN_NAME\", adapterNotes=\"\", type=DOUBLE, isNullable=true, isIdentity=false, defaultValue=\"default\"}")); + } + @Test void testEqualsAndHashContract() { EqualsVerifier.forClass(ColumnMetadata.class).verify(); diff --git a/src/test/java/com/exasol/adapter/request/AdapterTelemetryConfigurationTest.java b/src/test/java/com/exasol/adapter/request/AdapterTelemetryConfigurationTest.java index f27ef3eb..c4b61fd8 100644 --- a/src/test/java/com/exasol/adapter/request/AdapterTelemetryConfigurationTest.java +++ b/src/test/java/com/exasol/adapter/request/AdapterTelemetryConfigurationTest.java @@ -5,10 +5,11 @@ import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.provider.*; class AdapterTelemetryConfigurationTest { private final Map properties = new HashMap<>(); @@ -38,6 +39,21 @@ void testTelemetryIsDisabledWhenPropertyHasNonBooleanValue() { assertThat(createConfiguration().isTelemetryDisabled(), equalTo(true)); } + @ParameterizedTest + @MethodSource("trimmedTelemetryValues") + void testTelemetryPropertyValueIsTrimmedBeforeParsing(final String value, final boolean expectedDisabled) { + this.properties.put(AdapterTelemetryConfiguration.TELEMETRY_PROPERTY, value); + assertThat(createConfiguration().isTelemetryDisabled(), equalTo(expectedDisabled)); + } + + private static Stream trimmedTelemetryValues() { + return Stream.of( + Arguments.of(" true ", false), + Arguments.of(" false ", true), + Arguments.of("\ttrue\t", false), + Arguments.of("\nfalse\n", true)); + } + private AdapterTelemetryConfiguration createConfiguration() { return AdapterTelemetryConfiguration.parseFromProperties(properties); } diff --git a/src/test/java/com/exasol/adapter/request/parser/PushDownSqlParserTest.java b/src/test/java/com/exasol/adapter/request/parser/PushDownSqlParserTest.java index 25df0e89..f0884211 100644 --- a/src/test/java/com/exasol/adapter/request/parser/PushDownSqlParserTest.java +++ b/src/test/java/com/exasol/adapter/request/parser/PushDownSqlParserTest.java @@ -506,6 +506,27 @@ void testParsePredicateIsNull() { () -> assertThat(expression.getValue(), equalTo(0.0))); } + @Test + void testParsePredicateIsNullUsesLocaleIndependentUppercaseConversion() { + final Locale defaultLocale = Locale.getDefault(); + try { + Locale.setDefault(Locale.forLanguageTag("tr")); + final String sqlAsJson = "{" // + + " \"type\" : \"predicate_is_null\", " // + + " \"expression\" : { " // + + " \"type\" : \"literal_double\", " // + + " \"value\" : \"0.0\" " // + + " } " // + + "}"; + final JsonObject jsonObject = createJsonObjectFromString(sqlAsJson); + final SqlPredicateIsNull sqlPredicateIsNull = (SqlPredicateIsNull) this.defaultParser + .parseExpression(jsonObject); + assertThat(sqlPredicateIsNull.getType(), equalTo(PREDICATE_IS_NULL)); + } finally { + Locale.setDefault(defaultLocale); + } + } + @Test void testParsePredicateIsNotNull() { final String sqlAsJson = "{" // diff --git a/src/test/java/com/exasol/adapter/request/renderer/PushdownSqlRendererTest.java b/src/test/java/com/exasol/adapter/request/renderer/PushdownSqlRendererTest.java index bb1ed61c..b64d0f91 100644 --- a/src/test/java/com/exasol/adapter/request/renderer/PushdownSqlRendererTest.java +++ b/src/test/java/com/exasol/adapter/request/renderer/PushdownSqlRendererTest.java @@ -1,8 +1,15 @@ package com.exasol.adapter.request.renderer; -import static com.exasol.adapter.metadata.DataType.createDecimal; -import static com.exasol.adapter.metadata.DataType.createVarChar; +import static com.exasol.adapter.metadata.DataType.*; import static com.exasol.adapter.metadata.DataType.ExaCharset.UTF8; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.IOException; import java.io.StringReader; @@ -17,10 +24,11 @@ import org.junit.jupiter.params.provider.ValueSource; import org.skyscreamer.jsonassert.JSONAssert; +import com.exasol.adapter.AdapterException; import com.exasol.adapter.metadata.ColumnMetadata; import com.exasol.adapter.metadata.TableMetadata; import com.exasol.adapter.request.parser.PushdownSqlParser; -import com.exasol.adapter.sql.SqlNode; +import com.exasol.adapter.sql.*; import jakarta.json.*; @@ -901,4 +909,86 @@ void testParseGroupByWithSingleGroupColumnName() throws JSONException { + "}"; assertParseAndRenderGeneratesSameJson(sqlAsJson); } + + @Test + void testRenderWrapsAdapterException() throws AdapterException { + final SqlNode failingNode = mock(SqlNode.class); + when(failingNode.accept(any())).thenThrow(new AdapterException("Adapter error")); + final PushdownSqlRenderer renderer = new PushdownSqlRenderer(); + final IllegalStateException exception = assertThrows(IllegalStateException.class, () -> renderer.render(failingNode)); + assertAll(() -> assertThat(exception.getMessage(), equalTo( + "F-VSCOMJAVA-34: An unexpected error occurred during request serialization: 'Adapter error'. " + + "This is an internal error that should not happen. Please report it by opening a GitHub issue.")), + () -> assertThat(exception.getCause(), instanceOf(AdapterException.class))); + } + + @Test + void testRenderFunctionAggregateWithDistinct() { + final SqlFunctionAggregate node = new SqlFunctionAggregate(AggregateFunction.SUM, + List.of(new SqlLiteralString("1")), true); + final JsonObject rendered = renderToJsonObject(node); + + assertAll(() -> assertThat(rendered.getString("type"), equalTo("function_aggregate")), + () -> assertThat(rendered.getString("name"), equalTo("SUM")), + () -> assertThat(rendered.getBoolean("distinct"), equalTo(true))); + } + + @Test + void testRenderFunctionAggregateGroupConcatWithoutDistinct() { + final SqlFunctionAggregateGroupConcat node = SqlFunctionAggregateGroupConcat.builder(new SqlLiteralString("x")) + .separator(new SqlLiteralString(", ")).build(); + final JsonObject rendered = renderToJsonObject(node); + + assertAll(() -> assertThat(rendered.getString("type"), equalTo("function_aggregate_group_concat")), + () -> assertThat(rendered.getString("name"), equalTo("GROUP_CONCAT")), + () -> assertThat(rendered.containsKey("distinct"), equalTo(false)), + () -> assertThat(rendered.getJsonObject("separator").getString("type"), equalTo("literal_string")), + () -> assertThat(rendered.getJsonObject("separator").getString("value"), equalTo(", "))); + } + + @Test + void testRenderDataTypeDecimal() { + final JsonObject rendered = renderToJsonObject(new SqlFunctionScalarCast(createDecimal(9, 2), + new SqlLiteralString("dummy"))); + + assertAll(() -> assertThat(rendered.getJsonObject("dataType").getString("type"), equalTo("DECIMAL")), + () -> assertThat(rendered.getJsonObject("dataType").getJsonNumber("precision").intValue(), + equalTo(9)), + () -> assertThat(rendered.getJsonObject("dataType").getJsonNumber("scale").intValue(), equalTo(2))); + } + + @Test + void testRenderDataTypeTimestamp() { + final JsonObject rendered = renderToJsonObject(new SqlFunctionScalarCast(createTimestamp(true, 5), + new SqlLiteralString("dummy"))); + + assertAll(() -> assertThat(rendered.getJsonObject("dataType").getString("type"), equalTo("TIMESTAMP")), + () -> assertThat(rendered.getJsonObject("dataType").getBoolean("withLocalTimeZone"), + equalTo(true))); + } + + @Test + void testRenderDataTypeGeometry() { + final JsonObject rendered = renderToJsonObject(new SqlFunctionScalarCast(createGeometry(4326), + new SqlLiteralString("dummy"))); + + assertAll(() -> assertThat(rendered.getJsonObject("dataType").getString("type"), equalTo("GEOMETRY")), + () -> assertThat(rendered.getJsonObject("dataType").getJsonNumber("srid").intValue(), + equalTo(4326))); + } + + @Test + void testRenderDataTypeIntervalYearToMonth() { + final JsonObject rendered = renderToJsonObject(new SqlFunctionScalarCast(createIntervalYearMonth(7), + new SqlLiteralString("dummy"))); + + assertAll(() -> assertThat(rendered.getJsonObject("dataType").getString("type"), equalTo("INTERVAL")), + () -> assertThat(rendered.getJsonObject("dataType").getJsonNumber("precision").intValue(), equalTo(7)), + () -> assertThat(rendered.getJsonObject("dataType").getString("fromTo"), equalTo("YEAR TO MONTH"))); + } + + private JsonObject renderToJsonObject(final SqlNode node) { + return (JsonObject) new PushdownSqlRenderer().render(node); + } + }