diff --git a/doc/changes/changelog.md b/doc/changes/changelog.md
index 77f1ed1..1b384a7 100644
--- a/doc/changes/changelog.md
+++ b/doc/changes/changelog.md
@@ -1,5 +1,6 @@
# Changes
+* [10.3.0](changes_10.3.0.md)
* [10.2.0](changes_10.2.0.md)
* [10.1.0](changes_10.1.0.md)
* [10.0.2](changes_10.0.2.md)
diff --git a/doc/changes/changes_10.3.0.md b/doc/changes/changes_10.3.0.md
new file mode 100644
index 0000000..508466f
--- /dev/null
+++ b/doc/changes/changes_10.3.0.md
@@ -0,0 +1,27 @@
+# Virtual Schema Common JDBC 10.3.0, released 2023-03-10
+
+Code name: Escape Wildcards
+
+## Summary
+
+This release fixes ambiguous results by escaping SQL wildcards such as underscore `_` and percent `%` in names of catalogs, schemas, and tables when retrieving column metadata from JDBC driver.
+
+The release also adds a constructor enabling derived SQL dialects to add additional validators for adapter properties, hence removing the need to override method `AbstractSqlDialect.validateProperties()`.
+
+## Bugfixes
+
+* #136: Fixed column lookup for tables not escaping wildcards
+* #138: Enabled SQL dialects to add property validators
+
+## Dependency Updates
+
+### Compile Dependency Updates
+
+* Updated `com.exasol:error-reporting-java:1.0.0` to `1.0.1`
+
+### Test Dependency Updates
+
+* Updated `com.exasol:java-util-logging-testing:2.0.2` to `2.0.3`
+* Updated `nl.jqno.equalsverifier:equalsverifier:3.11.1` to `3.14`
+* Updated `org.junit.jupiter:junit-jupiter:5.9.1` to `5.9.2`
+* Updated `org.mockito:mockito-junit-jupiter:4.9.0` to `5.1.1`
diff --git a/pk_generated_parent.pom b/pk_generated_parent.pom
index df5d69b..14585a2 100644
--- a/pk_generated_parent.pom
+++ b/pk_generated_parent.pom
@@ -3,7 +3,7 @@
4.0.0
com.exasol
virtual-schema-common-jdbc-generated-parent
- 10.2.0
+ 10.3.0
pom
UTF-8
diff --git a/pom.xml b/pom.xml
index 94ba4bc..6ad5098 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
virtual-schema-common-jdbc
- 10.2.0
+ 10.3.0
Virtual Schema Common JDBC
Common module for JDBC-based data access from Virtual Schemas.
https://github.com/exasol/virtual-schema-common-jdbc/
@@ -15,12 +15,12 @@
com.exasol
error-reporting-java
- 1.0.0
+ 1.0.1
com.exasol
java-util-logging-testing
- 2.0.2
+ 2.0.3
test
@@ -38,19 +38,19 @@
org.junit.jupiter
junit-jupiter
- 5.9.1
+ 5.9.2
test
nl.jqno.equalsverifier
equalsverifier
- 3.11.1
+ 3.14
test
org.mockito
mockito-junit-jupiter
- 4.9.0
+ 5.1.1
test
@@ -122,7 +122,7 @@
virtual-schema-common-jdbc-generated-parent
com.exasol
- 10.2.0
+ 10.3.0
pk_generated_parent.pom
diff --git a/src/main/java/com/exasol/adapter/dialects/AbstractSqlDialect.java b/src/main/java/com/exasol/adapter/dialects/AbstractSqlDialect.java
index 31b5c5e..72a7ed9 100644
--- a/src/main/java/com/exasol/adapter/dialects/AbstractSqlDialect.java
+++ b/src/main/java/com/exasol/adapter/dialects/AbstractSqlDialect.java
@@ -46,6 +46,22 @@ public abstract class AbstractSqlDialect implements SqlDialect {
*/
protected AbstractSqlDialect(final ConnectionFactory connectionFactory, final AdapterProperties properties,
final Set dialectSpecificProperties) {
+ this(connectionFactory, properties, dialectSpecificProperties, List.of());
+ }
+
+ /**
+ * Create a new instance of an {@link AbstractSqlDialect}.
+ *
+ * @param connectionFactory factory for JDBC connection to remote data source
+ * @param properties user properties
+ * @param dialectSpecificProperties a set of properties that dialect supports additionally to the common set
+ * {@link com.exasol.adapter.dialects.AbstractSqlDialect#COMMON_SUPPORTED_PROPERTIES}
+ * @param dialectSpecificPropertyValidators a collection of property validators the dialect wants to apply
+ * additionally to the common validators
+ */
+ protected AbstractSqlDialect(final ConnectionFactory connectionFactory, final AdapterProperties properties,
+ final Set dialectSpecificProperties,
+ final Collection dialectSpecificPropertyValidators) {
this.connectionFactory = connectionFactory;
this.properties = properties;
this.supportedProperties = new SupportedPropertiesValidator() //
@@ -59,7 +75,8 @@ protected AbstractSqlDialect(final ConnectionFactory connectionFactory, final Ad
.add(PropertyValidator.forStructureElement(supportsJdbcSchemas(), "schemas", SCHEMA_NAME_PROPERTY))
.add(ExceptionHandlingProperty.validator()) //
.add(DataTypeDetection.getValidator()) //
- .add(TableCountLimit.getValidator());
+ .add(TableCountLimit.getValidator()) //
+ .addAll(dialectSpecificPropertyValidators);
}
/**
diff --git a/src/main/java/com/exasol/adapter/jdbc/BaseColumnMetadataReader.java b/src/main/java/com/exasol/adapter/jdbc/BaseColumnMetadataReader.java
index 138e76e..6429da0 100644
--- a/src/main/java/com/exasol/adapter/jdbc/BaseColumnMetadataReader.java
+++ b/src/main/java/com/exasol/adapter/jdbc/BaseColumnMetadataReader.java
@@ -75,7 +75,7 @@ public List mapColumns(final String tableName) {
/**
* Read the columns metadata from a result set.
- *
+ *
* @param catalogName catalog name
* @param schemaName schema name
* @param tableName table name
@@ -83,8 +83,7 @@ public List mapColumns(final String tableName) {
*/
protected List mapColumns(final String catalogName, final String schemaName,
final String tableName) {
- try (final ResultSet remoteColumns = this.connection.getMetaData().getColumns(catalogName, schemaName,
- tableName, ANY_COLUMN)) {
+ try (final ResultSet remoteColumns = getColumnMetadata(catalogName, schemaName, tableName)) {
return getColumnsFromResultSet(remoteColumns);
} catch (final SQLException exception) {
throw new RemoteMetadataReaderException(ExaError.messageBuilder("E-VSCJDBC-1").message(
@@ -93,9 +92,18 @@ protected List mapColumns(final String catalogName, final String
}
}
+ private ResultSet getColumnMetadata(final String catalogName, final String schemaName, final String tableName)
+ throws SQLException {
+ return this.connection.getMetaData().getColumns( //
+ catalogName == null ? null : Wildcards.escape(catalogName), //
+ schemaName == null ? null : Wildcards.escape(schemaName), //
+ Wildcards.escape(tableName), //
+ ANY_COLUMN);
+ }
+
/**
* Read the columns result set.
- *
+ *
* @param remoteColumns column result set.
* @return list of column metadata
* @throws SQLException if read fails
@@ -110,7 +118,7 @@ protected List getColumnsFromResultSet(final ResultSet remoteCol
/**
* Read the column metadata from result set if supported. Otherwise, skip.
- *
+ *
* @param remoteColumns column result set
* @param columns list to append column to
* @throws SQLException if read fails
@@ -150,7 +158,7 @@ private ColumnMetadata mapColumn(final ResultSet remoteColumn) throws SQLExcepti
/**
* Read the JDBC type description of a column.
- *
+ *
* @param remoteColumn result set column
* @return JDBC type description
* @throws SQLException if read fails
@@ -186,7 +194,7 @@ private String readTypeName(final ResultSet remoteColumn) throws SQLException {
/**
* Check if a column a nullable.
- *
+ *
* @param remoteColumn column result set
* @param columnName column name
* @return {@code true} if remote column is nullable
@@ -256,7 +264,7 @@ private String readColumnTypeName(final ResultSet remoteColumn) throws SQLExcept
/**
* Read the column name form result set.
- *
+ *
* @param columns column result set
* @return column name
* @throws SQLException if read fails
@@ -332,7 +340,7 @@ private static DataType convertBigInteger(final int jdbcPrecision) {
/**
* Build a data type for a decimal value.
- *
+ *
* @param jdbcPrecision precision
* @param scale scale
* @return built data type
@@ -374,7 +382,7 @@ private static DataType convertChar(final int size, final int octetLength) {
/**
* Convert the column name.
- *
+ *
* @param columnName column name
* @return column name
*/
@@ -404,7 +412,7 @@ protected DataType mapJdbcTypeNumericToDecimalWithFallbackToDouble(final JDBCTyp
/**
* Parse a number type property.
- *
+ *
* @param property formatted string: {@code .}
* @return data type
*/
diff --git a/src/main/java/com/exasol/adapter/jdbc/Wildcards.java b/src/main/java/com/exasol/adapter/jdbc/Wildcards.java
new file mode 100644
index 0000000..5f29e6d
--- /dev/null
+++ b/src/main/java/com/exasol/adapter/jdbc/Wildcards.java
@@ -0,0 +1,25 @@
+package com.exasol.adapter.jdbc;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Escape SQL wildcards in string to enable to request column metadata from JDBC driver with exact match for names of
+ * catalog, schema, and table.
+ */
+public class Wildcards {
+ private static final Pattern PATTERN = Pattern.compile("[_%]");
+
+ /**
+ * @param input Name of catalog, schema, or table
+ * @return name with potential wildcards escaped.
+ */
+ public static String escape(final String input) {
+ final Matcher matcher = PATTERN.matcher(input);
+ return matcher.find() ? matcher.replaceAll("\\\\$0") : input;
+ }
+
+ private Wildcards() {
+ // only static usage
+ }
+}
diff --git a/src/main/java/com/exasol/adapter/properties/ValidatorChain.java b/src/main/java/com/exasol/adapter/properties/ValidatorChain.java
index bcde36d..0ba499e 100644
--- a/src/main/java/com/exasol/adapter/properties/ValidatorChain.java
+++ b/src/main/java/com/exasol/adapter/properties/ValidatorChain.java
@@ -1,7 +1,6 @@
package com.exasol.adapter.properties;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
import com.exasol.adapter.AdapterProperties;
@@ -23,6 +22,17 @@ public ValidatorChain add(final PropertyValidator validator) {
return this;
}
+ /**
+ * Add a list of property validators to the validator chain.
+ *
+ * @param validators validators to add to the current chain of validators
+ * @return this for fluent programming
+ */
+ public ValidatorChain addAll(final Collection validators) {
+ this.propertyValidators.addAll(validators);
+ return this;
+ }
+
@Override
public void validate(final AdapterProperties properties) throws PropertyValidationException {
for (final PropertyValidator validator : this.propertyValidators) {
diff --git a/src/test/java/com/exasol/adapter/jdbc/BaseRemoteMetadataReaderTest.java b/src/test/java/com/exasol/adapter/jdbc/BaseRemoteMetadataReaderTest.java
index 46f0554..3adbca4 100644
--- a/src/test/java/com/exasol/adapter/jdbc/BaseRemoteMetadataReaderTest.java
+++ b/src/test/java/com/exasol/adapter/jdbc/BaseRemoteMetadataReaderTest.java
@@ -4,8 +4,10 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
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.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.sql.*;
@@ -86,7 +88,8 @@ private void mockTableA(final DatabaseMetaData remoteMetadataMock) throws SQLExc
when(tableAColumns.getString(BaseColumnMetadataReader.TYPE_NAME_COLUMN)).thenReturn("BOOLEAN", "DATE");
when(tableAColumns.getString(BaseColumnMetadataReader.NAME_COLUMN)).thenReturn("COLUMN_A1", "COLUMN_A2");
when(tableAColumns.getInt(BaseColumnMetadataReader.DATA_TYPE_COLUMN)).thenReturn(Types.BOOLEAN, Types.DATE);
- when(remoteMetadataMock.getColumns(any(), any(), eq(TABLE_A), any())).thenReturn(tableAColumns);
+ when(remoteMetadataMock.getColumns(any(), any(), eq(Wildcards.escape(TABLE_A)), any()))
+ .thenReturn(tableAColumns);
}
private void mockTableB(final DatabaseMetaData remoteMetadataMock) throws SQLException {
@@ -96,7 +99,8 @@ private void mockTableB(final DatabaseMetaData remoteMetadataMock) throws SQLExc
when(tableBColumns.getInt(BaseColumnMetadataReader.DATA_TYPE_COLUMN)).thenReturn(Types.BOOLEAN, Types.DOUBLE);
when(tableBColumns.getString(BaseColumnMetadataReader.NAME_COLUMN)).thenReturn("COLUMN_B1", "COLUMN_B2",
"COLUMN_B3");
- when(remoteMetadataMock.getColumns(any(), any(), eq(TABLE_B), any())).thenReturn(tableBColumns);
+ when(remoteMetadataMock.getColumns(any(), any(), eq(Wildcards.escape(TABLE_B)), any()))
+ .thenReturn(tableBColumns);
}
private void mockGetTableCalls(final DatabaseMetaData remoteMetadataMock) throws SQLException {
@@ -252,4 +256,29 @@ void testGetSupportedTableTypes() {
assertThat(new BaseRemoteMetadataReader(null, AdapterProperties.emptyProperties()).getSupportedTableTypes(),
containsInAnyOrder("TABLE", "VIEW", "SYSTEM TABLE"));
}
+
+ @Test
+ void testEscapeSchemaName() throws SQLException {
+ verifyEscapeSchemaOrCatalog(null, "THE\\_SCHEMA", Map.of("SCHEMA_NAME", "THE_SCHEMA"));
+ }
+
+ @Test
+ void testEscapeCatalogName() throws SQLException {
+ verifyEscapeSchemaOrCatalog("THE\\_CATALOG", null, Map.of("CATALOG_NAME", "THE_CATALOG"));
+ }
+
+ void verifyEscapeSchemaOrCatalog(final String cat, final String schema, final Map properties)
+ throws SQLException {
+ final DatabaseMetaData metadataMock = mock(DatabaseMetaData.class);
+ final Connection connection = mock(Connection.class);
+ when(connection.getMetaData()).thenReturn(metadataMock);
+ when(metadataMock.getColumns(cat, schema, "THE\\_TABLE", "%")).thenThrow(new SpecialException());
+ final BaseColumnMetadataReader testee = new BaseColumnMetadataReader(connection,
+ new AdapterProperties(properties), null);
+ assertThrows(SpecialException.class, () -> testee.mapColumns("THE_TABLE"));
+ }
+
+ static class SpecialException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+ }
}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/adapter/jdbc/ColumnMetadataReaderTest.java b/src/test/java/com/exasol/adapter/jdbc/ColumnMetadataReaderTest.java
index 5bade85..fe7abbc 100644
--- a/src/test/java/com/exasol/adapter/jdbc/ColumnMetadataReaderTest.java
+++ b/src/test/java/com/exasol/adapter/jdbc/ColumnMetadataReaderTest.java
@@ -65,14 +65,14 @@ private ColumnMetadata mapSingleMockedColumn(final String originalTypeName) thro
when(this.columnsMock.next()).thenReturn(true, false);
when(this.columnsMock.getString(NAME_COLUMN)).thenReturn(originalTypeName + "_COLUMN");
when(this.columnsMock.getString(TYPE_NAME_COLUMN)).thenReturn(originalTypeName);
- when(this.remoteMetadataMock.getColumns(null, null, "THE_TABLE", "%")).thenReturn(this.columnsMock);
+ when(this.remoteMetadataMock.getColumns(null, null, "THE\\_TABLE", "%")).thenReturn(this.columnsMock);
final List columns = mapMockedColumns(this.columnsMock);
return columns.get(0);
}
private List mapMockedColumns(final ResultSet columnsMock)
throws RemoteMetadataReaderException, SQLException {
- when(this.remoteMetadataMock.getColumns(null, null, "THE_TABLE", "%")).thenReturn(columnsMock);
+ when(this.remoteMetadataMock.getColumns(null, null, "THE\\_TABLE", "%")).thenReturn(columnsMock);
return createDefaultColumnMetadataReader().mapColumns("THE_TABLE");
}
@@ -348,7 +348,7 @@ void testMapColumnWithTypeNameNull() throws SQLException {
when(this.columnsMock.next()).thenReturn(true, false);
when(this.columnsMock.getString(NAME_COLUMN)).thenReturn("DOUBLE_COLUMN");
when(this.columnsMock.getString(TYPE_NAME_COLUMN)).thenReturn(null);
- when(this.remoteMetadataMock.getColumns(null, null, "THE_TABLE", "%")).thenReturn(this.columnsMock);
+ when(this.remoteMetadataMock.getColumns(null, null, "THE\\_TABLE", "%")).thenReturn(this.columnsMock);
final List columns = mapMockedColumns(this.columnsMock);
final ColumnMetadata columnMetadata = columns.get(0);
assertThat(columnMetadata.getOriginalTypeName(), equalTo(""));
diff --git a/src/test/java/com/exasol/adapter/jdbc/WildcardsTest.java b/src/test/java/com/exasol/adapter/jdbc/WildcardsTest.java
new file mode 100644
index 0000000..72a5542
--- /dev/null
+++ b/src/test/java/com/exasol/adapter/jdbc/WildcardsTest.java
@@ -0,0 +1,27 @@
+package com.exasol.adapter.jdbc;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+class WildcardsTest {
+ @ParameterizedTest
+ @CsvSource(value = { //
+ "abc, abc", //
+ "a _ a, a \\_ a", //
+ "a % a, a \\% a", //
+ "a _ b _ c, a \\_ b \\_ c", //
+ "a_b%c, a\\_b\\%c", //
+ })
+ void test(final String input, final String expected) {
+ assertThat(Wildcards.escape(input), equalTo(expected));
+ }
+
+ @Test
+ void testEmpty() {
+ assertThat(Wildcards.escape(""), equalTo(""));
+ }
+}