From 3aa41549c6e049587715c0d56f606ab8eec2cb7a Mon Sep 17 00:00:00 2001 From: Gopal Lal Date: Wed, 1 Apr 2026 21:53:11 +0530 Subject: [PATCH] Fix SEA metadata parity: escaped catalog in getSchemas, null foreign in getCrossReference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two SEA/Thrift parity fixes: 1. getSchemas: Strip JDBC escapes and lowercase the catalog parameter before populating the TABLE_CATALOG result column. SHOW SCHEMAS IN `catalog` doesn't return a catalog column from the server, so the client populates it — but was using the raw JDBC-escaped value (e.g., "comparator\_tests" instead of "comparator_tests"). 2. getCrossReference: Return empty result set when all three foreign key parameters (catalog, schema, table) are null, instead of throwing DatabricksValidationException. Matches Thrift server behavior which delegates to getExportedKeys (returns empty in DBSQL). Note: Other null-catalog/null-schema parity gaps exist for getCrossReference, getImportedKeys, and getPrimaryKeys when individual parameters are null (Thrift resolves them to current catalog/schema via the server). These need a broader auto-fill fix and are tracked separately. Co-authored-by: Isaac Signed-off-by: Gopal Lal --- .../DatabricksMetadataQueryClient.java | 20 ++++++- .../DatabricksMetadataQueryClientTest.java | 59 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksMetadataQueryClient.java b/src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksMetadataQueryClient.java index 91672edc1..2a3add237 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksMetadataQueryClient.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksMetadataQueryClient.java @@ -9,6 +9,7 @@ import com.databricks.jdbc.common.MetadataOperationType; import com.databricks.jdbc.common.StatementType; import com.databricks.jdbc.common.util.JdbcThreadUtils; +import com.databricks.jdbc.common.util.WildcardUtil; import com.databricks.jdbc.dbclient.IDatabricksClient; import com.databricks.jdbc.dbclient.IDatabricksMetadataClient; import com.databricks.jdbc.dbclient.impl.common.CommandConstants; @@ -98,9 +99,16 @@ public DatabricksResultSet listSchemas( new CommandBuilder(catalog, session).setSchemaPattern(schemaNamePattern); String SQL = commandBuilder.getSQLString(CommandName.LIST_SCHEMAS); LOGGER.debug("SQL command to fetch schemas: {}", SQL); + // Strip JDBC escape sequences from catalog for the result set TABLE_CATALOG column. + // SHOW SCHEMAS IN `catalog` doesn't return a catalog column from the server, + // so the client populates it from this parameter. Without stripping, JDBC-escaped + // underscores (\_) would appear in the result (e.g., "comparator\_tests" instead + // of "comparator_tests"). + String resultCatalog = + catalog != null ? WildcardUtil.stripJdbcEscapes(catalog).toLowerCase() : null; try { return metadataResultSetBuilder.getSchemasResult( - getResultSet(SQL, session, MetadataOperationType.GET_SCHEMAS), catalog); + getResultSet(SQL, session, MetadataOperationType.GET_SCHEMAS), resultCatalog); } catch (SQLException e) { if (catalog == null && PARSE_SYNTAX_ERROR_SQL_STATE.equals(e.getSQLState())) { // This is a fallback for the case where the SQL command fails with "syntax error at or near @@ -426,6 +434,16 @@ public DatabricksResultSet listCrossReferences( return metadataResultSetBuilder.getCrossRefsResult(new ArrayList<>()); } + // When all three foreign-side parameters are null, SHOW FOREIGN KEYS cannot be constructed. + // Match Thrift server behavior which delegates to getExportedKeys in this case + // (returns 0 rows since exported keys are not tracked in DBSQL). + if (foreignCatalog == null && foreignSchema == null && foreignTable == null) { + LOGGER.debug( + "All foreign key parameters are null for getCrossReference, " + + "returning empty result set to match Thrift behavior."); + return metadataResultSetBuilder.getCrossRefsResult(new ArrayList<>()); + } + CommandBuilder commandBuilder = new CommandBuilder(foreignCatalog, session).setSchema(foreignSchema).setTable(foreignTable); String SQL = commandBuilder.getSQLString(CommandName.LIST_FOREIGN_KEYS); diff --git a/src/test/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksMetadataQueryClientTest.java b/src/test/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksMetadataQueryClientTest.java index 6600d3902..f903eb7f8 100644 --- a/src/test/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksMetadataQueryClientTest.java +++ b/src/test/java/com/databricks/jdbc/dbclient/impl/sqlexec/DatabricksMetadataQueryClientTest.java @@ -458,6 +458,50 @@ void testListSchemas(String sqlStatement, String schema, String description) thr ((DatabricksResultSetMetaData) actualResult.getMetaData()).getTotalRows(), 1, description); } + /** + * Tests that getSchemas with a JDBC-escaped, mixed-case catalog name returns the unescaped, + * lowercased catalog name in the TABLE_CATALOG column. This reproduces the SEA/Thrift parity + * issue where SHOW SCHEMAS IN `catalog` doesn't return a catalog column from the server, so the + * client populates it from the parameter — which must be unescaped and lowercased. + */ + @Test + void testListSchemasWithEscapedUnderscoreCatalog() throws SQLException { + String escapedCatalog = "Comparator\\_Tests"; + String expectedCatalog = "comparator_tests"; + // CommandBuilder strips escapes for SQL: SHOW SCHEMAS IN `Comparator_Tests` + String expectedSQL = "SHOW SCHEMAS IN `Comparator_Tests`"; + + when(session.getComputeResource()).thenReturn(mockedComputeResource); + DatabricksMetadataQueryClient metadataClient = new DatabricksMetadataQueryClient(mockClient); + when(mockClient.executeStatement( + eq(expectedSQL), + eq(mockedComputeResource), + any(), + eq(StatementType.METADATA), + eq(session), + any(), + any(MetadataOperationType.class))) + .thenReturn(mockedResultSet); + when(mockedResultSet.next()).thenReturn(true, false); + when(mockedResultSet.getObject("databaseName")).thenReturn("default"); + doReturn(2).when(mockedMetaData).getColumnCount(); + doReturn(SCHEMA_COLUMN.getResultSetColumnName()).when(mockedMetaData).getColumnName(1); + doReturn(CATALOG_COLUMN.getResultSetColumnName()).when(mockedMetaData).getColumnName(2); + when(mockedResultSet.getMetaData()).thenReturn(mockedMetaData); + // SHOW SCHEMAS IN `catalog` doesn't return a catalog column — the client must populate it + when(mockedResultSet.findColumn(CATALOG_RESULT_COLUMN.getResultSetColumnName())) + .thenThrow(DatabricksSQLException.class); + + DatabricksResultSet actualResult = metadataClient.listSchemas(session, escapedCatalog, null); + + assertTrue(actualResult.next()); + // TABLE_CATALOG (column 2) should be unescaped and lowercased + assertEquals( + expectedCatalog, + actualResult.getObject(2), + "TABLE_CATALOG should be unescaped and lowercased to match Thrift behavior"); + } + @Test void testListSchemasNullCatalog() throws SQLException { when(session.getComputeResource()).thenReturn(mockedComputeResource); @@ -717,6 +761,21 @@ void testListCrossReferences() throws Exception { } } + /** + * Tests that getCrossReference returns empty result set (not an exception) when all three + * foreign-side parameters are null. Matches Thrift server behavior where null foreign table + * delegates to getExportedKeys which returns empty in DBSQL. + */ + @Test + void testListCrossReferences_allForeignParamsNull_returnsEmpty() throws Exception { + DatabricksMetadataQueryClient metadataClient = new DatabricksMetadataQueryClient(mockClient); + + DatabricksResultSet result = + metadataClient.listCrossReferences( + session, TEST_CATALOG, TEST_SCHEMA, TEST_TABLE, null, null, null); + assertFalse(result.next(), "Should return empty when all foreign params are null, not throw"); + } + @Test void testListCrossReferences_throwsParseSyntaxError() throws Exception { DatabricksSQLException exception =