From 1f0fb524f109619b954f832b561a6bf098ebc530 Mon Sep 17 00:00:00 2001 From: Christoph Pirkl Date: Tue, 2 Jun 2026 09:18:51 +0200 Subject: [PATCH 1/2] #318: Fix naming, Javadoc, and minor parsing cleanups --- doc/changes/changes_18.0.2.md | 2 ++ .../com/exasol/adapter/AdapterContext.java | 4 ++-- .../adapter/metadata/ColumnMetadata.java | 3 ++- .../AdapterTelemetryConfiguration.java | 3 ++- .../request/parser/PushdownSqlParser.java | 17 ++++++++------- .../adapter/request/parser/RequestParser.java | 2 +- .../parser/RequestParserConstants.java | 4 ++-- .../request/renderer/PushdownSqlRenderer.java | 2 +- .../adapter/metadata/ColumnMetadataTest.java | 6 ++++++ .../AdapterTelemetryConfigurationTest.java | 18 +++++++++++++++- .../request/parser/PushDownSqlParserTest.java | 21 +++++++++++++++++++ 11 files changed, 65 insertions(+), 17 deletions(-) diff --git a/doc/changes/changes_18.0.2.md b/doc/changes/changes_18.0.2.md index 00a63aaa..29937019 100644 --- a/doc/changes/changes_18.0.2.md +++ b/doc/changes/changes_18.0.2.md @@ -23,6 +23,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. ## 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 63b578fc..ec7c9a4f 100644 --- a/src/main/java/com/exasol/adapter/request/parser/PushdownSqlParser.java +++ b/src/main/java/com/exasol/adapter/request/parser/PushdownSqlParser.java @@ -480,7 +480,7 @@ private List collectInvolvedTables(final SqlNode from) { } 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)); @@ -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)); final Behavior overflowBehavior = new Behavior(behaviorType); if (behaviorType == BehaviorType.TRUNCATE) { overflowBehavior.setTruncationType( @@ -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..051a0628 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,7 @@ 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.").ticketMitigation().toString(), exception); } } 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 c8c885c1..0e564a93 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 = "{" // From 96bf756640a418e07b1ae6c286f38948b7595945 Mon Sep 17 00:00:00 2001 From: Christoph Pirkl Date: Tue, 2 Jun 2026 09:40:32 +0200 Subject: [PATCH 2/2] Improve test coverage --- .../request/renderer/PushdownSqlRenderer.java | 72 +++++++------- .../renderer/PushdownSqlRendererTest.java | 96 ++++++++++++++++++- 2 files changed, 130 insertions(+), 38 deletions(-) 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 051a0628..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("An 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/request/renderer/PushdownSqlRendererTest.java b/src/test/java/com/exasol/adapter/request/renderer/PushdownSqlRendererTest.java index 2bfde9ed..599e7eb4 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.*; @@ -879,4 +887,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); + } + }