getTableMetadata(final ResultSet remoteTables, f
throws SQLException {
final TableMetadata tableMetadata = mapTable(remoteTables, tableName);
if (tableHasColumns(tableMetadata)) {
- LOGGER.finer(() -> "Read table metadata: " + tableMetadata.describe());
return Optional.of(tableMetadata);
} else {
logSkippingTableWithEmptyColumns(tableName);
diff --git a/src/main/java/com/exasol/adapter/jdbc/JDBCAdapter.java b/src/main/java/com/exasol/adapter/jdbc/JDBCAdapter.java
index 0c2525f..7021bd6 100644
--- a/src/main/java/com/exasol/adapter/jdbc/JDBCAdapter.java
+++ b/src/main/java/com/exasol/adapter/jdbc/JDBCAdapter.java
@@ -61,7 +61,7 @@ public CreateVirtualSchemaResponse createVirtualSchema(final ExaMetadata exasolM
* @param request create request
*/
protected void logCreateVirtualSchemaRequestReceived(final CreateVirtualSchemaRequest request) {
- LOGGER.fine(() -> "Received request to create Virutal Schema \"" + request.getVirtualSchemaName() + "\".");
+ LOGGER.fine(() -> "Received request to create Virtual Schema \"" + request.getVirtualSchemaName() + "\".");
}
private AdapterProperties getPropertiesFromRequest(final AdapterRequest request) {
diff --git a/src/main/java/com/exasol/adapter/jdbc/WildcardEscaper.java b/src/main/java/com/exasol/adapter/jdbc/WildcardEscaper.java
new file mode 100644
index 0000000..6c26118
--- /dev/null
+++ b/src/main/java/com/exasol/adapter/jdbc/WildcardEscaper.java
@@ -0,0 +1,45 @@
+package com.exasol.adapter.jdbc;
+
+import java.sql.DatabaseMetaData;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Escape SQL wild cards in string to enable to request column metadata from JDBC driver with exact match for names of
+ * catalog, schema, and table.
+ */
+public class WildcardEscaper {
+
+ private static final Pattern REGEX_WILDCARDS = Pattern.compile("[\\\\$]");
+ private static final Pattern SQL_WINDCARDS = Pattern.compile("[_%]");
+
+ /**
+ * Create a new instance of the {@link WildcardEscaper}.
+ *
+ * Use {@link DatabaseMetaData#getSearchStringEscape()} to get the escape string for wild card characters.
+ *
+ * @param searchStringEscape string that should be used to escape SQL wild cards
+ * @return new instance of the {@link WildcardEscaper}
+ */
+ public static WildcardEscaper instance(final String searchStringEscape) {
+ final String escaped = new WildcardEscaper(REGEX_WILDCARDS, "\\\\").escape(searchStringEscape);
+ return new WildcardEscaper(SQL_WINDCARDS, escaped);
+ }
+
+ private final Pattern pattern;
+ private final String replacement;
+
+ WildcardEscaper(final Pattern pattern, final String replacement) {
+ this.pattern = pattern;
+ this.replacement = replacement + "$0";
+ }
+
+ /**
+ * @param input Name of catalog, schema, or table
+ * @return name with potential wild cards escaped.
+ */
+ public String escape(final String input) {
+ final Matcher matcher = this.pattern.matcher(input);
+ return matcher.find() ? matcher.replaceAll(this.replacement) : input;
+ }
+}
diff --git a/src/main/java/com/exasol/adapter/jdbc/Wildcards.java b/src/main/java/com/exasol/adapter/jdbc/Wildcards.java
deleted file mode 100644
index 5f29e6d..0000000
--- a/src/main/java/com/exasol/adapter/jdbc/Wildcards.java
+++ /dev/null
@@ -1,25 +0,0 @@
-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/test/java/com/exasol/adapter/jdbc/BaseRemoteMetadataReaderTest.java b/src/test/java/com/exasol/adapter/jdbc/BaseRemoteMetadataReaderTest.java
index 3adbca4..8f0270a 100644
--- a/src/test/java/com/exasol/adapter/jdbc/BaseRemoteMetadataReaderTest.java
+++ b/src/test/java/com/exasol/adapter/jdbc/BaseRemoteMetadataReaderTest.java
@@ -27,10 +27,11 @@
class BaseRemoteMetadataReaderTest {
private static final String IDENTIFIER_QUOTE_STRING = "identifier-quote-string";
private static final String CATALOG_SEPARATOR = "catalog-separator";
+ private static final String ESCAPE_STRING = "\\";
@Test
void testReadEmptyRemoteMetadata() throws RemoteMetadataReaderException, SQLException {
- final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata();
+ final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata(false);
final Connection connectionMock = mockConnection(remoteMetadataMock);
mockGetAllTablesReturnsEmptyList(remoteMetadataMock);
assertThat(readMockedSchemaMetadata(connectionMock).getTables(), emptyIterableOf(TableMetadata.class));
@@ -60,7 +61,7 @@ private SchemaMetadata readMockedSchemaMetadataWithProperties(final Connection c
@Test
void testReadRemoteMetadata() throws RemoteMetadataReaderException, SQLException {
- final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata();
+ final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata(true);
final Connection connectionMock = mockConnection(remoteMetadataMock);
mockGetColumnsCalls(remoteMetadataMock);
mockGetTableCalls(remoteMetadataMock);
@@ -88,7 +89,7 @@ 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(Wildcards.escape(TABLE_A)), any()))
+ when(remoteMetadataMock.getColumns(any(), any(), eq(escapeSqlWildCards(TABLE_A)), any()))
.thenReturn(tableAColumns);
}
@@ -99,10 +100,14 @@ 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(Wildcards.escape(TABLE_B)), any()))
+ when(remoteMetadataMock.getColumns(any(), any(), eq(escapeSqlWildCards(TABLE_B)), any()))
.thenReturn(tableBColumns);
}
+ private String escapeSqlWildCards(final String string) {
+ return WildcardEscaper.instance(ESCAPE_STRING).escape(string);
+ }
+
private void mockGetTableCalls(final DatabaseMetaData remoteMetadataMock) throws SQLException {
final ResultSet tablesMock = Mockito.mock(ResultSet.class);
mockTableCount(tablesMock, 2);
@@ -115,7 +120,7 @@ private void mockGetAllTables(final DatabaseMetaData remoteMetadataMock, final R
when(remoteMetadataMock.getTables(any(), any(), any(), any())).thenReturn(tables);
}
- protected DatabaseMetaData mockSupportingMetadata() throws SQLException {
+ protected DatabaseMetaData mockSupportingMetadata(final boolean mockGetSearchStringEscape) throws SQLException {
final DatabaseMetaData remoteMetadataMock = Mockito.mock(DatabaseMetaData.class);
when(remoteMetadataMock.getCatalogSeparator()).thenReturn(CATALOG_SEPARATOR);
when(remoteMetadataMock.getIdentifierQuoteString()).thenReturn(IDENTIFIER_QUOTE_STRING);
@@ -131,12 +136,15 @@ protected DatabaseMetaData mockSupportingMetadata() throws SQLException {
when(remoteMetadataMock.nullsAreSortedAtStart()).thenReturn(true);
when(remoteMetadataMock.nullsAreSortedHigh()).thenReturn(true);
when(remoteMetadataMock.nullsAreSortedLow()).thenReturn(true);
+ if (mockGetSearchStringEscape) {
+ when(remoteMetadataMock.getSearchStringEscape()).thenReturn(ESCAPE_STRING);
+ }
return remoteMetadataMock;
}
@Test
void testReadRemoteDataSkippingFilteredTables() throws SQLException {
- final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata();
+ final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata(true);
final Connection connectionMock = mockConnection(remoteMetadataMock);
final Map rawProperties = new HashMap<>();
rawProperties.put(AdapterProperties.TABLE_FILTER_PROPERTY, TABLE_B);
@@ -152,7 +160,7 @@ void testReadRemoteDataSkippingFilteredTables() throws SQLException {
@Test
void testCreateSchemaAdapterNotes() throws SQLException {
- final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata();
+ final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata(false);
final Connection connectionMock = mockConnection(remoteMetadataMock);
final RemoteMetadataReader reader = new BaseRemoteMetadataReader(connectionMock,
AdapterProperties.emptyProperties());
@@ -175,7 +183,7 @@ void testCreateSchemaAdapterNotes() throws SQLException {
@Test
void testReadRemoteMetadataWithAdapterNotes() throws RemoteMetadataReaderException, SQLException {
- final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata();
+ final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata(true);
final Connection connectionMock = mockConnection(remoteMetadataMock);
final ResultSet tablesMock = Mockito.mock(ResultSet.class);
mockTableCount(tablesMock, 1);
@@ -227,7 +235,7 @@ void testGetSchemaNameFilter() {
// work together.
@Test
void testReadRemoteDataSkippingForSelectedTablesOnly() throws SQLException {
- final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata();
+ final DatabaseMetaData remoteMetadataMock = mockSupportingMetadata(true);
final Connection connectionMock = mockConnection(remoteMetadataMock);
mockGetTableCalls(remoteMetadataMock);
mockTableA(remoteMetadataMock);
@@ -259,20 +267,23 @@ void testGetSupportedTableTypes() {
@Test
void testEscapeSchemaName() throws SQLException {
- verifyEscapeSchemaOrCatalog(null, "THE\\_SCHEMA", Map.of("SCHEMA_NAME", "THE_SCHEMA"));
+ verifyEscapeSchemaOrCatalog(null, escapeSqlWildCards("THE_SCHEMA"), Map.of("SCHEMA_NAME", "THE_SCHEMA"));
}
@Test
void testEscapeCatalogName() throws SQLException {
- verifyEscapeSchemaOrCatalog("THE\\_CATALOG", null, Map.of("CATALOG_NAME", "THE_CATALOG"));
+ 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);
+ when(metadataMock.getSearchStringEscape()).thenReturn(ESCAPE_STRING);
final Connection connection = mock(Connection.class);
when(connection.getMetaData()).thenReturn(metadataMock);
- when(metadataMock.getColumns(cat, schema, "THE\\_TABLE", "%")).thenThrow(new SpecialException());
+ String tableName = escapeSqlWildCards("THE_TABLE");
+ when(metadataMock.getColumns(cat, schema, tableName, "%"))
+ .thenThrow(new SpecialException());
final BaseColumnMetadataReader testee = new BaseColumnMetadataReader(connection,
new AdapterProperties(properties), null);
assertThrows(SpecialException.class, () -> testee.mapColumns("THE_TABLE"));
diff --git a/src/test/java/com/exasol/adapter/jdbc/ColumnMetadataReaderTest.java b/src/test/java/com/exasol/adapter/jdbc/ColumnMetadataReaderTest.java
index fe7abbc..fa23dec 100644
--- a/src/test/java/com/exasol/adapter/jdbc/ColumnMetadataReaderTest.java
+++ b/src/test/java/com/exasol/adapter/jdbc/ColumnMetadataReaderTest.java
@@ -6,6 +6,7 @@
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import java.sql.*;
@@ -35,6 +36,8 @@ class ColumnMetadataReaderTest {
ExaCharset.UTF8);
private static final DataType TYPE_MAX_VARCHAR_ASCII = DataType.createVarChar(DataType.MAX_EXASOL_VARCHAR_SIZE,
ExaCharset.ASCII);
+ private static final String ESCAPE_STRING = "\\";
+
@Mock
private Connection connectionMock;
@Mock
@@ -44,7 +47,8 @@ class ColumnMetadataReaderTest {
@BeforeEach
void beforeEach() throws SQLException {
- when(this.connectionMock.getMetaData()).thenReturn(this.remoteMetadataMock);
+ lenient().when(this.connectionMock.getMetaData()).thenReturn(this.remoteMetadataMock);
+ lenient().when(this.remoteMetadataMock.getSearchStringEscape()).thenReturn(ESCAPE_STRING);
}
@Test
diff --git a/src/test/java/com/exasol/adapter/jdbc/WildcardsTest.java b/src/test/java/com/exasol/adapter/jdbc/WildcardEscaperTest.java
similarity index 51%
rename from src/test/java/com/exasol/adapter/jdbc/WildcardsTest.java
rename to src/test/java/com/exasol/adapter/jdbc/WildcardEscaperTest.java
index 72a5542..bcae92c 100644
--- a/src/test/java/com/exasol/adapter/jdbc/WildcardsTest.java
+++ b/src/test/java/com/exasol/adapter/jdbc/WildcardEscaperTest.java
@@ -6,8 +6,9 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
-class WildcardsTest {
+class WildcardEscaperTest {
@ParameterizedTest
@CsvSource(value = { //
"abc, abc", //
@@ -17,11 +18,21 @@ class WildcardsTest {
"a_b%c, a\\_b\\%c", //
})
void test(final String input, final String expected) {
- assertThat(Wildcards.escape(input), equalTo(expected));
+ assertThat(testee("\\").escape(input), equalTo(expected));
}
@Test
void testEmpty() {
- assertThat(Wildcards.escape(""), equalTo(""));
+ assertThat(testee("\\").escape(""), equalTo(""));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "\\", "$", "/" })
+ void testEscapeCharacter(final String escapeCharacter) {
+ assertThat(testee(escapeCharacter).escape("a_b"), equalTo("a" + escapeCharacter + "_b"));
+ }
+
+ private WildcardEscaper testee(final String escapeCharacter) {
+ return WildcardEscaper.instance(escapeCharacter);
}
}