Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/changelog.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions doc/changes/changes_18.0.3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Common Module of Exasol Virtual Schemas Adapters 18.0.3, released 2026-06-08

Code name: Fix locale-dependent request serialization

## Summary

This release consistently uses `Locale.ROOT` for converting strings to upper/lower case. This avoids locale dependent bugs.

## Bugfixes

* #335: Fixed incomplete `Locale.ROOT` hardening in request parsing and rendering. Locale-sensitive casing is now avoided when parsing table metadata and `IS JSON` constraints, and when rendering pushdown JSON for node types, data types, character sets, and join types.
2 changes: 1 addition & 1 deletion pk_generated_parent.pom

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>virtual-schema-common-java</artifactId>
<version>18.0.2</version>
<version>18.0.3</version>
<name>Common module of Exasol Virtual Schemas Adapters</name>
<description>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.</description>
Expand Down Expand Up @@ -93,7 +93,7 @@
<parent>
<artifactId>virtual-schema-common-java-generated-parent</artifactId>
<groupId>com.exasol</groupId>
<version>18.0.2</version>
<version>18.0.3</version>
<relativePath>pk_generated_parent.pom</relativePath>
</parent>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

import static com.exasol.adapter.request.parser.RequestParserConstants.*;

import java.util.ArrayList;
import java.util.List;
import java.util.*;

import com.exasol.adapter.metadata.*;
import com.exasol.adapter.metadata.converter.SchemaMetadataJsonConverter;
Expand Down Expand Up @@ -222,7 +221,7 @@ private static DataType.IntervalType intervalTypeFromString(final String interva
}

private DataType getDataType(final JsonObject dataType, final String columnDescription) {
final String typeName = requireString(dataType, "type", columnDescription + " data type").toUpperCase();
final String typeName = requireString(dataType, "type", columnDescription + " data type").toUpperCase(Locale.ROOT);
switch (typeName) {
case "DECIMAL":
return getDecimalDataType(dataType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static com.exasol.adapter.request.RequestJsonKeys.*;

import java.util.List;
import java.util.Locale;
import java.util.Optional;

import com.exasol.adapter.AdapterException;
Expand Down Expand Up @@ -58,7 +59,7 @@ public JsonObject visit(final SqlStatementSelect select) throws AdapterException
}

private JsonObjectBuilder createObjectBuilderFor(final SqlNode type) {
return JSON.createObjectBuilder().add(TYPE, type.getType().name().toLowerCase());
return JSON.createObjectBuilder().add(TYPE, type.getType().name().toLowerCase(Locale.ROOT));
}

private void addIfPresent(final SqlNode node, final String key, final JsonObjectBuilder builder)
Expand Down Expand Up @@ -159,15 +160,15 @@ public JsonObject visit(final SqlFunctionScalarCast function) throws AdapterExce
private JsonObject render(final DataType dataType) {
final JsonObjectBuilder builder = JSON.createObjectBuilder();
final DataType.ExaDataType type = dataType.getExaDataType();
builder.add(TYPE, type.name().toUpperCase());
builder.add(TYPE, type.name().toUpperCase(Locale.ROOT));
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(CHARACTER_SET, dataType.getCharset().name().toLowerCase(Locale.ROOT));
builder.add(SIZE, dataType.getSize());
break;
case TIMESTAMP:
Expand Down Expand Up @@ -462,7 +463,7 @@ public JsonObject visit(final SqlJoin sqlJoin) throws AdapterException {
builder.add(LEFT, sqlJoin.getLeft().accept(this));
builder.add(RIGHT, sqlJoin.getRight().accept(this));
builder.add(CONDITION, sqlJoin.getCondition().accept(this));
builder.add(JOIN_TYPE, sqlJoin.getJoinType().name().toLowerCase());
builder.add(JOIN_TYPE, sqlJoin.getJoinType().name().toLowerCase(Locale.ROOT));
return builder.build();
}

Expand Down
22 changes: 11 additions & 11 deletions src/main/java/com/exasol/adapter/sql/AbstractSqlPredicateJson.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.exasol.adapter.sql;

import java.util.Arrays;
import java.util.List;
import java.util.*;

/**
* This class contains a common behavior for the {@link SqlNodeType#PREDICATE_IS_JSON} and
Expand All @@ -15,11 +14,11 @@ public abstract class AbstractSqlPredicateJson extends SqlPredicate {
/**
* The Type constraint.
*/
protected final SqlPredicateIsJson.TypeConstraints typeConstraint;
protected final TypeConstraints typeConstraint;
/**
* The Key uniqueness constraint.
*/
protected final SqlPredicateIsJson.KeyUniquenessConstraint keyUniquenessConstraint;
protected final KeyUniquenessConstraint keyUniquenessConstraint;

/**
* Instantiates a new Abstract sql predicate json.
Expand All @@ -29,9 +28,8 @@ public abstract class AbstractSqlPredicateJson extends SqlPredicate {
* @param typeConstraint the type constraint
* @param keyUniquenessConstraint the key uniqueness constraint
*/
public AbstractSqlPredicateJson(final Predicate predicate, final SqlNode expression,
final SqlPredicateIsJson.TypeConstraints typeConstraint,
final SqlPredicateIsJson.KeyUniquenessConstraint keyUniquenessConstraint) {
protected AbstractSqlPredicateJson(final Predicate predicate, final SqlNode expression, final TypeConstraints typeConstraint,
final KeyUniquenessConstraint keyUniquenessConstraint) {
super(predicate);
this.expression = expression;
this.typeConstraint = typeConstraint;
Expand Down Expand Up @@ -117,12 +115,14 @@ public String toString() {
* @param value the value
* @return the sql predicate is json . key uniqueness constraint
*/
public static SqlPredicateIsJson.KeyUniquenessConstraint of(final String value) {
final String formattedValue = String.join("_", value.split(" ")).toUpperCase();
return SqlPredicateIsJson.KeyUniquenessConstraint.valueOf(formattedValue);
public static KeyUniquenessConstraint of(final String value) {
final String formattedValue = String.join("_", value.split(" ")).toUpperCase(Locale.ROOT);
return KeyUniquenessConstraint.valueOf(formattedValue);
}
}

@Override
public List<SqlNode> getChildren() { return Arrays.asList(this.expression); }
public List<SqlNode> getChildren() {
return Arrays.asList(this.expression);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

import com.exasol.adapter.metadata.*;
import com.exasol.adapter.sql.*;
import com.exasol.test.locale.WithLocale;

import jakarta.json.*;

Expand Down Expand Up @@ -537,24 +538,19 @@ void testParsePredicateIsNull() {
}

@Test
@WithLocale("tr")
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);
}
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));
}

@Test
Expand Down Expand Up @@ -665,47 +661,47 @@ void testParseFunctionScalarCastToTimestamp() {

@Test
void testGetDataTypeDecimal() {
assertThat(getDataType("{"
+ " \"type\" : \"DECIMAL\", "
+ " \"precision\" : 31, "
+ " \"scale\" : 41 "
assertThat(getDataType("{"
+ " \"type\" : \"DECIMAL\", "
+ " \"precision\" : 31, "
+ " \"scale\" : 41 "
+ "}"), equalTo(createDecimal(31, 41)));
}

@Test
void testGetDataTypeDouble() {
assertThat(getDataType("{"
+ " \"type\" : \"DOUBLE\" "
assertThat(getDataType("{"
+ " \"type\" : \"DOUBLE\" "
+ "}"), equalTo(DataType.createDouble()));
}

@Test
void testGetDataTypeChar() {
assertThat(getDataType("{"
+ " \"type\" : \"CHAR\", "
+ " \"size\" : 22 "
assertThat(getDataType("{"
+ " \"type\" : \"CHAR\", "
+ " \"size\" : 22 "
+ "}"), equalTo(DataType.createChar(22, UTF8)));
}

@Test
void testGetDataTypeBoolean() {
assertThat(getDataType("{"
+ " \"type\" : \"BOOLEAN\" "
assertThat(getDataType("{"
+ " \"type\" : \"BOOLEAN\" "
+ "}"), equalTo(DataType.createBool()));
}

@Test
void testGetDataTypeDate() {
assertThat(getDataType("{"
+ " \"type\" : \"DATE\" "
assertThat(getDataType("{"
+ " \"type\" : \"DATE\" "
+ "}"), equalTo(DataType.createDate()));
}

@Test
void testGetDataTypeGeometry() {
assertThat(getDataType("{"
+ " \"type\" : \"GEOMETRY\", "
+ " \"srid\" : 42 "
assertThat(getDataType("{"
+ " \"type\" : \"GEOMETRY\", "
+ " \"srid\" : 42 "
+ "}"), equalTo(DataType.createGeometry(42)));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.junit.jupiter.params.provider.MethodSource;

import com.exasol.adapter.metadata.*;
import com.exasol.test.locale.WithLocale;

import jakarta.json.*;

Expand Down Expand Up @@ -191,6 +192,26 @@ void testParseMetadataReadsAdapterNotes() {
.identity(false).defaultValue("").comment("").build()));
}

@Test
@WithLocale("tr")
void testParseMetadataUsesLocaleIndependentUppercaseConversion() {
final JsonArray tablesAsJson = arrayBuilder().add(objectBuilder()
.add("name", "T1")
.add("columns", arrayBuilder()
.add(objectBuilder().add("name", "INTERVAL_COL")
.add("dataType", objectBuilder().add("type", "interval").add("fromTo",
"YEAR TO MONTH")))))
.build();

final List<TableMetadata> tables = TablesMetadataParser.create().parse(tablesAsJson);

final List<ColumnMetadata> expectedColumns = List.of(ColumnMetadata.builder().name("INTERVAL_COL")
.adapterNotes("").type(DataType.createIntervalYearMonth(2)).nullable(true).identity(false)
.defaultValue("").comment("").build());

assertThat(tables, contains(new TableMetadata("T1", "", expectedColumns, "")));
}

@ParameterizedTest
@MethodSource("invalidTableMetadata")
void testParseMetadataThrowsException(final JsonArray tablesAsJson,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.exasol.adapter.request.renderer;

import static com.exasol.adapter.metadata.DataType.*;
import static com.exasol.adapter.metadata.DataType.ExaCharset.ASCII;
import static com.exasol.adapter.metadata.DataType.ExaCharset.UTF8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
Expand Down Expand Up @@ -29,6 +30,7 @@
import com.exasol.adapter.metadata.TableMetadata;
import com.exasol.adapter.request.parser.PushdownSqlParser;
import com.exasol.adapter.sql.*;
import com.exasol.test.locale.WithLocale;

import jakarta.json.*;

Expand Down Expand Up @@ -987,6 +989,51 @@ void testRenderDataTypeIntervalYearToMonth() {
() -> assertThat(rendered.getJsonObject("dataType").getString("fromTo"), equalTo("YEAR TO MONTH")));
}

@Test
@WithLocale("tr")
void testRenderUsesLocaleIndependentLowercaseForJoinTypes() {
final JsonObject rendered = renderToJsonObject(new SqlJoin(new SqlTable("A", null), new SqlTable("B", null),
new SqlLiteralBool(true), JoinType.INNER));

assertAll(() -> assertThat(rendered.getString("type"), equalTo("join")),
() -> assertThat(rendered.getString("join_type"), equalTo("inner")));
}

@Test
@WithLocale("tr")
void testRenderUsesLocaleIndependentCharacterSetAndDataTypeNames() {
final JsonObject rendered = renderToJsonObject(
new SqlFunctionScalarCast(createVarChar(10, ASCII), new SqlLiteralString("dummy")));
final JsonObject dataType = rendered.getJsonObject("dataType");

assertAll(() -> assertThat(dataType.getString("type"), equalTo("VARCHAR")),
() -> assertThat(dataType.getString("characterSet"), equalTo("ascii")));
}

@Test
@WithLocale("tr")
void testRenderUsesLocaleIndependentUppercaseForDataTypeNames() {
final JsonObject rendered = renderToJsonObject(
new SqlFunctionScalarCast(createIntervalYearMonth(7), new SqlLiteralString("dummy")));
final JsonObject dataType = rendered.getJsonObject("dataType");

assertAll(() -> assertThat(dataType.getString("type"), equalTo("INTERVAL")),
() -> assertThat(dataType.getJsonNumber("precision").intValue(), equalTo(7)),
() -> assertThat(dataType.getString("fromTo"), equalTo("YEAR TO MONTH")));
}

@Test
@WithLocale("tr")
void testRenderUsesLocaleIndependentLowercaseForPredicateTypes() {
final JsonObject rendered = renderToJsonObject(new SqlPredicateIsJson(new SqlLiteralString("x"),
AbstractSqlPredicateJson.TypeConstraints.OBJECT,
AbstractSqlPredicateJson.KeyUniquenessConstraint.WITH_UNIQUE_KEYS));

assertAll(() -> assertThat(rendered.getString("type"), equalTo("predicate_is_json")),
() -> assertThat(rendered.getString("typeConstraint"), equalTo("OBJECT")),
() -> assertThat(rendered.getString("keyUniquenessConstraint"), equalTo("WITH UNIQUE KEYS")));
}

private JsonObject renderToJsonObject(final SqlNode node) {
return (JsonObject) new PushdownSqlRenderer().render(node);
}
Expand Down
Loading