From bf139a880fa10610c071cd073cd8d4fcb17baf78 Mon Sep 17 00:00:00 2001 From: Dominik Przybysz Date: Thu, 19 Dec 2024 12:46:37 +0100 Subject: [PATCH] SNOW-1636286: Add exact search for schema --- .../snowflake/client/core/SFBaseSession.java | 13 ++ .../net/snowflake/client/core/SFSession.java | 7 + .../client/core/SFSessionProperty.java | 1 + .../jdbc/SnowflakeDatabaseMetaData.java | 132 ++++++++++-------- .../net/snowflake/client/util/SFTriple.java | 51 +++++++ .../client/jdbc/DatabaseMetaDataLatestIT.java | 69 ++++++++- 6 files changed, 209 insertions(+), 64 deletions(-) create mode 100644 src/main/java/net/snowflake/client/util/SFTriple.java diff --git a/src/main/java/net/snowflake/client/core/SFBaseSession.java b/src/main/java/net/snowflake/client/core/SFBaseSession.java index a7b374fde..4b53ba015 100644 --- a/src/main/java/net/snowflake/client/core/SFBaseSession.java +++ b/src/main/java/net/snowflake/client/core/SFBaseSession.java @@ -139,6 +139,11 @@ public abstract class SFBaseSession { // we need to allow for it to maintain backwards compatibility. private boolean enablePatternSearch = true; + // Enables the use of exact schema searches for certain DatabaseMetaData methods + // that should use schema from context (CLIENT_METADATA_REQUEST_USE_CONNECTION_CTX=true) + // value is false for backwards compatibility. + private boolean enableExactSchemaSearch = false; + /** Disable lookup for default credentials by GCS library */ private boolean disableGcsDefaultCredentials = false; @@ -1069,6 +1074,14 @@ public void setEnablePatternSearch(boolean enablePatternSearch) { this.enablePatternSearch = enablePatternSearch; } + public boolean getEnableExactSchemaSearch() { + return enableExactSchemaSearch; + } + + void setEnableExactSchemaSearch(boolean enableExactSchemaSearch) { + this.enableExactSchemaSearch = enableExactSchemaSearch; + } + public boolean getDisableGcsDefaultCredentials() { return disableGcsDefaultCredentials; } diff --git a/src/main/java/net/snowflake/client/core/SFSession.java b/src/main/java/net/snowflake/client/core/SFSession.java index ecef55de3..2b53af840 100644 --- a/src/main/java/net/snowflake/client/core/SFSession.java +++ b/src/main/java/net/snowflake/client/core/SFSession.java @@ -509,6 +509,13 @@ public void addSFSessionProperty(String propertyName, Object propertyValue) thro setEnablePatternSearch(getBooleanValue(propertyValue)); } break; + + case ENABLE_EXACT_SCHEMA_SEARCH_ENABLED: + if (propertyValue != null) { + setEnableExactSchemaSearch(getBooleanValue(propertyValue)); + } + break; + case DISABLE_GCS_DEFAULT_CREDENTIALS: if (propertyValue != null) { setDisableGcsDefaultCredentials(getBooleanValue(propertyValue)); diff --git a/src/main/java/net/snowflake/client/core/SFSessionProperty.java b/src/main/java/net/snowflake/client/core/SFSessionProperty.java index a5e7276c5..60f8a63c1 100644 --- a/src/main/java/net/snowflake/client/core/SFSessionProperty.java +++ b/src/main/java/net/snowflake/client/core/SFSessionProperty.java @@ -89,6 +89,7 @@ public enum SFSessionProperty { DIAGNOSTICS_ALLOWLIST_FILE("DIAGNOSTICS_ALLOWLIST_FILE", false, String.class), ENABLE_PATTERN_SEARCH("enablePatternSearch", false, Boolean.class), + ENABLE_EXACT_SCHEMA_SEARCH_ENABLED("ENABLE_EXACT_SCHEMA_SEARCH_ENABLED", false, Boolean.class), DISABLE_GCS_DEFAULT_CREDENTIALS("disableGcsDefaultCredentials", false, Boolean.class), diff --git a/src/main/java/net/snowflake/client/jdbc/SnowflakeDatabaseMetaData.java b/src/main/java/net/snowflake/client/jdbc/SnowflakeDatabaseMetaData.java index 88ee255e1..ad0f0fa7b 100644 --- a/src/main/java/net/snowflake/client/jdbc/SnowflakeDatabaseMetaData.java +++ b/src/main/java/net/snowflake/client/jdbc/SnowflakeDatabaseMetaData.java @@ -47,7 +47,7 @@ import net.snowflake.client.log.ArgSupplier; import net.snowflake.client.log.SFLogger; import net.snowflake.client.log.SFLoggerFactory; -import net.snowflake.client.util.SFPair; +import net.snowflake.client.util.SFTriple; import net.snowflake.common.core.SqlState; import net.snowflake.common.util.Wildcard; @@ -167,6 +167,7 @@ public class SnowflakeDatabaseMetaData implements DatabaseMetaData { // Indicates if pattern matching is allowed for all parameters. private boolean isPatternMatchingEnabled = true; + private boolean exactSchemaSearchEnabled; SnowflakeDatabaseMetaData(Connection connection) throws SQLException { logger.trace("SnowflakeDatabaseMetaData(SnowflakeConnection connection)", false); @@ -179,6 +180,7 @@ public class SnowflakeDatabaseMetaData implements DatabaseMetaData { this.ibInstance = session.getTelemetryClient(); this.procedureResultsetColumnNum = -1; this.isPatternMatchingEnabled = session.getEnablePatternSearch(); + this.exactSchemaSearchEnabled = session.getEnableExactSchemaSearch(); } private void raiseSQLExceptionIfConnectionIsClosed() throws SQLException { @@ -1390,7 +1392,8 @@ else if (i == 0) { } // apply session context when catalog is unspecified - private SFPair applySessionContext(String catalog, String schemaPattern) { + private SFTriple applySessionContext( + String catalog, String schemaPattern) { if (metadataRequestUseConnectionCtx) { // CLIENT_METADATA_USE_SESSION_DATABASE = TRUE if (catalog == null) { @@ -1407,7 +1410,7 @@ private SFPair applySessionContext(String catalog, String schema } } } - return SFPair.of(catalog, schemaPattern); + return new SFTriple<>(catalog, schemaPattern, exactSchemaSearchEnabled && useSessionSchema); } /* helper function for getProcedures, getFunctionColumns, etc. Returns sql command to show some type of result such @@ -1415,9 +1418,10 @@ private SFPair applySessionContext(String catalog, String schema private String getFirstResultSetCommand( String catalog, String schemaPattern, String name, String type) { // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(catalog, schemaPattern); - catalog = resPair.left; - schemaPattern = resPair.right; + SFTriple result = applySessionContext(catalog, schemaPattern); + catalog = result.first(); + schemaPattern = result.second(); + boolean isExactSchema = result.third(); String showProcedureCommand = "show /* JDBC:DatabaseMetaData.getProcedures() */ " + type; @@ -1431,7 +1435,7 @@ private String getFirstResultSetCommand( return ""; } else { String catalogEscaped = escapeSqlQuotes(catalog); - if (schemaPattern == null || isSchemaNameWildcardPattern(schemaPattern)) { + if (!isExactSchema && (schemaPattern == null || isSchemaNameWildcardPattern(schemaPattern))) { showProcedureCommand += " in database \"" + catalogEscaped + "\""; } else if (schemaPattern.isEmpty()) { return ""; @@ -1510,9 +1514,11 @@ public ResultSet getTables( return SnowflakeDatabaseMetaDataResultSet.getEmptyResultSet(GET_TABLES, statement); } - SFPair resPair = applySessionContext(originalCatalog, originalSchemaPattern); - final String catalog = resPair.left; - final String schemaPattern = resPair.right; + SFTriple result = + applySessionContext(originalCatalog, originalSchemaPattern); + String catalog = result.first(); + String schemaPattern = result.second(); + boolean isExactSchema = result.third(); final Pattern compiledSchemaPattern = Wildcard.toRegexPattern(schemaPattern, true); final Pattern compiledTablePattern = Wildcard.toRegexPattern(tableNamePattern, true); @@ -1553,7 +1559,7 @@ public ResultSet getTables( } else if (schemaPattern.isEmpty()) { return SnowflakeDatabaseMetaDataResultSet.getEmptyResultSet(GET_TABLES, statement); } else { - String schemaUnescaped = unescapeChars(schemaPattern); + String schemaUnescaped = isExactSchema ? schemaPattern : unescapeChars(schemaPattern); showTablesCommand += " in schema \"" + catalogEscaped + "\".\"" + schemaUnescaped + "\""; } } @@ -1699,9 +1705,11 @@ public ResultSet getColumns( Statement statement = connection.createStatement(); // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(originalCatalog, originalSchemaPattern); - final String catalog = resPair.left; - final String schemaPattern = resPair.right; + SFTriple result = + applySessionContext(originalCatalog, originalSchemaPattern); + String catalog = result.first(); + String schemaPattern = result.second(); + boolean isExactSchema = result.third(); final Pattern compiledSchemaPattern = Wildcard.toRegexPattern(schemaPattern, true); final Pattern compiledTablePattern = Wildcard.toRegexPattern(tableNamePattern, true); @@ -1729,7 +1737,7 @@ public ResultSet getColumns( return SnowflakeDatabaseMetaDataResultSet.getEmptyResultSet( extendedSet ? GET_COLUMNS_EXTENDED_SET : GET_COLUMNS, statement); } else { - String schemaUnescaped = unescapeChars(schemaPattern); + String schemaUnescaped = isExactSchema ? schemaPattern : unescapeChars(schemaPattern); if (tableNamePattern == null || Wildcard.isWildcardPatternStr(tableNamePattern)) { showColumnsCommand += " in schema \"" + catalogEscaped + "\".\"" + schemaUnescaped + "\""; } else if (tableNamePattern.isEmpty()) { @@ -1968,9 +1976,11 @@ public ResultSet getTablePrivileges( return SnowflakeDatabaseMetaDataResultSet.getEmptyResultSet(GET_TABLE_PRIVILEGES, statement); } // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(originalCatalog, originalSchemaPattern); - final String catalog = resPair.left; - final String schemaPattern = resPair.right; + SFTriple result = + applySessionContext(originalCatalog, originalSchemaPattern); + String catalog = result.first(); + String schemaPattern = result.second(); + boolean isExactSchema = result.third(); String showView = "select * from "; @@ -1993,10 +2003,11 @@ public ResultSet getTablePrivileges( && !schemaPattern.isEmpty() && !schemaPattern.trim().equals("%") && !schemaPattern.trim().equals(".*")) { + String unescapedSchema = isExactSchema ? schemaPattern : unescapeChars(schemaPattern); if (showView.contains("where table_name")) { - showView += " and table_schema = '" + unescapeChars(schemaPattern) + "'"; + showView += " and table_schema = '" + unescapedSchema + "'"; } else { - showView += " where table_schema = '" + unescapeChars(schemaPattern) + "'"; + showView += " where table_schema = '" + unescapedSchema + "'"; } } showView += " order by table_catalog, table_schema, table_name, privilege_type"; @@ -2087,9 +2098,10 @@ public ResultSet getPrimaryKeys(String originalCatalog, String originalSchema, f String showPKCommand = "show /* JDBC:DatabaseMetaData.getPrimaryKeys() */ primary keys in "; // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(originalCatalog, originalSchema); - final String catalog = resPair.left; - final String schema = resPair.right; + SFTriple result = applySessionContext(originalCatalog, originalSchema); + String catalog = result.first(); + String schema = result.second(); + boolean isExactSchema = result.third(); // These Patterns will only be used if the connection property enablePatternSearch=true final Pattern compiledSchemaPattern = Wildcard.toRegexPattern(schema, true); @@ -2106,7 +2118,7 @@ public ResultSet getPrimaryKeys(String originalCatalog, String originalSchema, f } else if (schema.isEmpty()) { return SnowflakeDatabaseMetaDataResultSet.getEmptyResultSet(GET_PRIMARY_KEYS, statement); } else { - String schemaUnescaped = unescapeChars(schema); + String schemaUnescaped = isExactSchema ? schema : unescapeChars(schema); if (table == null) { showPKCommand += "schema \"" + catalogUnescaped + "\".\"" + schemaUnescaped + "\""; } else if (table.isEmpty()) { @@ -2250,11 +2262,11 @@ private ResultSet getForeignKeys( StringBuilder commandBuilder = new StringBuilder(); // apply session context when catalog is unspecified - // apply session context when catalog is unspecified - SFPair resPair = + SFTriple result = applySessionContext(originalParentCatalog, originalParentSchema); - final String parentCatalog = resPair.left; - final String parentSchema = resPair.right; + String parentCatalog = result.first(); + String parentSchema = result.second(); + boolean isExactSchema = result.third(); // These Patterns will only be used to filter results if the connection property // enablePatternSearch=true @@ -2283,7 +2295,7 @@ private ResultSet getForeignKeys( } else if (parentSchema.isEmpty()) { return SnowflakeDatabaseMetaDataResultSet.getEmptyResultSet(GET_FOREIGN_KEYS, statement); } else { - String unescapedParentSchema = unescapeChars(parentSchema); + String unescapedParentSchema = isExactSchema ? parentSchema : unescapeChars(parentSchema); if (parentTable == null) { commandBuilder.append( "schema \"" + unescapedParentCatalog + "\".\"" + unescapedParentSchema + "\""); @@ -2580,12 +2592,7 @@ public ResultSet getImportedKeys(String originalCatalog, String originalSchema, originalSchema, table); - // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(originalCatalog, originalSchema); - final String catalog = resPair.left; - final String schema = resPair.right; - - return getForeignKeys("import", catalog, schema, table, null, null, null); + return getForeignKeys("import", originalCatalog, originalSchema, table, null, null, null); } @Override @@ -2598,10 +2605,6 @@ public ResultSet getExportedKeys(String catalog, String schema, String table) schema, table); - // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(catalog, schema); - catalog = resPair.left; - schema = resPair.right; return getForeignKeys("export", catalog, schema, table, null, null, null); } @@ -2625,11 +2628,6 @@ public ResultSet getCrossReference( foreignCatalog, foreignSchema, foreignTable); - // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(parentCatalog, parentSchema); - parentCatalog = resPair.left; - parentSchema = resPair.right; - return getForeignKeys( "cross", parentCatalog, @@ -2877,9 +2875,11 @@ public ResultSet getStreams( Statement statement = connection.createStatement(); // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(originalCatalog, originalSchemaPattern); - final String catalog = resPair.left; - final String schemaPattern = resPair.right; + SFTriple result = + applySessionContext(originalCatalog, originalSchemaPattern); + String catalog = result.first(); + String schemaPattern = result.second(); + boolean isExactSchema = result.third(); final Pattern compiledSchemaPattern = Wildcard.toRegexPattern(schemaPattern, true); final Pattern compiledStreamNamePattern = Wildcard.toRegexPattern(streamName, true); @@ -2904,7 +2904,7 @@ public ResultSet getStreams( } else if (schemaPattern.isEmpty()) { return SnowflakeDatabaseMetaDataResultSet.getEmptyResultSet(GET_STREAMS, statement); } else { - String schemaUnescaped = unescapeChars(schemaPattern); + String schemaUnescaped = isExactSchema ? schemaPattern : unescapeChars(schemaPattern); if (streamName == null || Wildcard.isWildcardPatternStr(streamName)) { showStreamsCommand += " in schema \"" + catalogEscaped + "\".\"" + schemaUnescaped + "\""; } @@ -3280,35 +3280,44 @@ public ResultSet getSchemas(String originalCatalog, String originalSchema) throw raiseSQLExceptionIfConnectionIsClosed(); // apply session context when catalog is unspecified - SFPair resPair = applySessionContext(originalCatalog, originalSchema); - final String catalog = resPair.left; - final String schemaPattern = resPair.right; + SFTriple result = applySessionContext(originalCatalog, originalSchema); + final String catalog = result.first(); + final String schemaPattern = result.second(); + boolean isExactSchema = result.third(); final Pattern compiledSchemaPattern = Wildcard.toRegexPattern(schemaPattern, true); - String showSchemas = "show /* JDBC:DatabaseMetaData.getSchemas() */ schemas"; + StringBuilder showSchemas = + new StringBuilder("show /* JDBC:DatabaseMetaData.getSchemas() */ schemas"); Statement statement = connection.createStatement(); - // only add pattern if it is not empty and not matching all character. - if (schemaPattern != null + if (isExactSchema) { + String escapedSchema = + schemaPattern.replaceAll("_", "\\\\\\\\_").replaceAll("%", "\\\\\\\\%"); + showSchemas.append(" like '").append(escapedSchema).append("'"); + } else if (schemaPattern != null && !schemaPattern.isEmpty() && !schemaPattern.trim().equals("%") && !schemaPattern.trim().equals(".*")) { - showSchemas += " like '" + escapeSingleQuoteForLikeCommand(schemaPattern) + "'"; + // only add pattern if it is not empty and not matching all character. + showSchemas + .append(" like '") + .append(escapeSingleQuoteForLikeCommand(schemaPattern)) + .append("'"); } if (catalog == null) { - showSchemas += " in account"; + showSchemas.append(" in account"); } else if (catalog.isEmpty()) { return SnowflakeDatabaseMetaDataResultSet.getEmptyResultSet(GET_SCHEMAS, statement); } else { - showSchemas += " in database \"" + escapeSqlQuotes(catalog) + "\""; + showSchemas.append(" in database \"").append(escapeSqlQuotes(catalog)).append("\""); } - logger.debug("Sql command to get schemas metadata: {}", showSchemas); + String sqlQuery = showSchemas.toString(); + logger.debug("Sql command to get schemas metadata: {}", sqlQuery); - ResultSet resultSet = - executeAndReturnEmptyResultIfNotFound(statement, showSchemas, GET_SCHEMAS); + ResultSet resultSet = executeAndReturnEmptyResultIfNotFound(statement, sqlQuery, GET_SCHEMAS); sendInBandTelemetryMetadataMetrics( resultSet, "getSchemas", originalCatalog, originalSchema, "none", "none"); return new SnowflakeDatabaseMetaDataQueryResultSet(GET_SCHEMAS, resultSet, statement) { @@ -3323,7 +3332,8 @@ public boolean next() throws SQLException { String dbName = showObjectResultSet.getString(5); if (compiledSchemaPattern == null - || compiledSchemaPattern.matcher(schemaName).matches()) { + || compiledSchemaPattern.matcher(schemaName).matches() + || isExactSchema) { nextRow[0] = schemaName; nextRow[1] = dbName; return true; diff --git a/src/main/java/net/snowflake/client/util/SFTriple.java b/src/main/java/net/snowflake/client/util/SFTriple.java new file mode 100644 index 000000000..4d8fbfaab --- /dev/null +++ b/src/main/java/net/snowflake/client/util/SFTriple.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024 Snowflake Computing Inc. All rights reserved. + */ +package net.snowflake.client.util; + +import java.util.Objects; +import net.snowflake.client.core.SnowflakeJdbcInternalApi; + +@SnowflakeJdbcInternalApi +public class SFTriple { + private final F first; + private final S second; + private final T third; + + public SFTriple(F first, S second, T third) { + this.first = first; + this.second = second; + this.third = third; + } + + public F first() { + return first; + } + + public S second() { + return second; + } + + public T third() { + return third; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + SFTriple sfTriple = (SFTriple) o; + return Objects.equals(first, sfTriple.first) + && Objects.equals(second, sfTriple.second) + && Objects.equals(third, sfTriple.third); + } + + @Override + public int hashCode() { + return Objects.hash(first, second, third); + } + + @Override + public String toString() { + return "[ " + first + ", " + second + ", " + third + " ]"; + } +} diff --git a/src/test/java/net/snowflake/client/jdbc/DatabaseMetaDataLatestIT.java b/src/test/java/net/snowflake/client/jdbc/DatabaseMetaDataLatestIT.java index c038be49e..a0aa24200 100644 --- a/src/test/java/net/snowflake/client/jdbc/DatabaseMetaDataLatestIT.java +++ b/src/test/java/net/snowflake/client/jdbc/DatabaseMetaDataLatestIT.java @@ -29,12 +29,14 @@ import java.util.HashSet; import java.util.Map; import java.util.Properties; +import java.util.Random; import java.util.Set; import net.snowflake.client.TestUtil; import net.snowflake.client.annotations.DontRunOnGithubActions; import net.snowflake.client.category.TestTags; import net.snowflake.client.core.SFBaseSession; import net.snowflake.client.core.SFSessionProperty; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Tag; @@ -80,10 +82,11 @@ public class DatabaseMetaDataLatestIT extends BaseJDBCWithSharedConnectionIT { private static final String ENABLE_PATTERN_SEARCH = SFSessionProperty.ENABLE_PATTERN_SEARCH.getPropertyKey(); - private static final String startingSchema; - private static final String startingDatabase; + private static String startingSchema; + private static String startingDatabase; - static { + @BeforeAll + public static void prepare() { try { startingSchema = connection.getSchema(); startingDatabase = connection.getCatalog(); @@ -2336,6 +2339,7 @@ public void testKeywordsCount() throws SQLException { DatabaseMetaData metaData = connection.getMetaData(); assertEquals(43, metaData.getSQLKeywords().split(",").length); } + /** Added in > 3.16.1 */ @Test public void testVectorDimension() throws SQLException { @@ -2371,4 +2375,63 @@ public void testVectorDimension() throws SQLException { } } } + + /** + * Added in > 3.21.0 for SNOW-1619625 - we need to use exact schema when + * CLIENT_METADATA_REQUEST_USE_CONNECTION_CTX = true + */ + @Test + public void testExactSchemaSearching() throws SQLException { + Random random = new Random(); + int suffix = random.nextInt(Integer.MAX_VALUE); + String schemaName = "_ESCAPED_UNDERSCORE_" + suffix; + String alternativeSchemaName = schemaName.replaceAll("_", "x"); + try (Connection connection = getConnection(); + Statement statement = connection.createStatement()) { + statement.execute("CREATE SCHEMA \"" + schemaName + "\""); + statement.execute("CREATE SCHEMA \"" + alternativeSchemaName + "\""); + statement.execute( + "CREATE TABLE \"" + + connection.getCatalog() + + "\".\"" + + schemaName + + "\".\"mytable\" (a text)"); + statement.execute( + "CREATE TABLE \"" + + connection.getCatalog() + + "\".\"" + + alternativeSchemaName + + "\".\"mytable\" (a text)"); + try { + Properties props = new Properties(); + props.put("CLIENT_METADATA_REQUEST_USE_CONNECTION_CTX", true); + props.put("ENABLE_EXACT_SCHEMA_SEARCH_ENABLED", true); + props.put("schema", schemaName); // we should not escape schema here + try (Connection connectionWithContext = getConnection(props); + ResultSet schemas = connectionWithContext.getMetaData().getSchemas(); + ResultSet schemasWithPattern = + connectionWithContext.getMetaData().getSchemas(null, null); + ResultSet tables = + connectionWithContext.getMetaData().getTables(null, null, null, null)) { + assertEquals(schemaName, connectionWithContext.getSchema()); + assertTrue(schemas.next()); + String schemaNameColumnInMetadata = "TABLE_SCHEM"; // it's not typo + assertEquals(schemaName, schemas.getString(schemaNameColumnInMetadata)); + assertFalse(schemas.next()); + + assertTrue(schemasWithPattern.next()); + assertEquals(schemaName, schemasWithPattern.getString(schemaNameColumnInMetadata)); + assertFalse(schemasWithPattern.next()); + + assertTrue(tables.next()); + assertEquals("mytable", tables.getString("TABLE_NAME")); + assertEquals(schemaName, tables.getString(schemaNameColumnInMetadata)); + assertFalse(tables.next()); + } + } finally { + statement.execute("DROP SCHEMA \"" + schemaName + "\""); + statement.execute("DROP SCHEMA \"" + alternativeSchemaName + "\""); + } + } + } }