diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index e12c8875f..1b131a186 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -8,6 +8,8 @@ - `DatabaseMetaData.getColumns(...)` with a `null` catalog now issues a single `SHOW COLUMNS IN ALL CATALOGS` statement (consistent with `getSchemas`/`getTables`) instead of enumerating every catalog and issuing a per-catalog `SHOW COLUMNS`. Older DBR versions that do not support the syntax transparently fall back to the previous enumerate-and-fan-out behavior. ### Fixed +- Fixed `DatabaseMetaData.getTables(...)` treating an empty `types` array (`new String[]{}`) as "match no table types" (returning zero rows). Per the JDBC contract, an empty or `null` type list carries no type constraint and now matches all table types, identical to passing `null`. Affects both the SEA and Thrift metadata paths. + - Fixed `IdleConnectionEvictor` thread leak in long-running applications. Driver-side resources (HTTP client, background threads) are now always released when `Connection.close()` is called, even if statement cleanup or server-side session termination fails. - Throw `DatabricksSQLException` instead of an unchecked `ClassCastException` when a complex-type getter (`getArray`, `getStruct`, `getMap`) is called on a column of a different complex type. 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 695faf67d..2ced180b6 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 @@ -141,9 +141,10 @@ public DatabricksResultSet listTables( String tableNamePattern, String[] tableTypes) throws SQLException { - // Per JDBC spec: null types = return all types; empty array = return nothing + // Per JDBC spec: a null or empty types list carries no type constraint and matches all table + // types. Normalize an empty array to null so it behaves identically to null (match-all). if (tableTypes != null && tableTypes.length == 0) { - return metadataResultSetBuilder.getTablesResult(catalog, tableTypes, new ArrayList<>()); + tableTypes = null; } String[] validatedTableTypes = tableTypes != null ? tableTypes : DEFAULT_TABLE_TYPES; diff --git a/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClient.java b/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClient.java index 1eb7c82aa..ecabf81b3 100644 --- a/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClient.java +++ b/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClient.java @@ -512,9 +512,10 @@ public DatabricksResultSet listTables( session.toString(), catalog, schemaNamePattern, tableNamePattern); LOGGER.debug(context); - // Per JDBC spec: null types = return all types; empty array = return nothing + // Per JDBC spec: a null or empty types list carries no type constraint and matches all table + // types. Normalize an empty array to null so it behaves identically to null (match-all). if (tableTypes != null && tableTypes.length == 0) { - return metadataResultSetBuilder.getTablesResult(catalog, tableTypes, new ArrayList<>()); + tableTypes = null; } if (!metadataResultSetBuilder.shouldAllowCatalogAccess(catalog, null, session)) { diff --git a/src/test/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClientTest.java b/src/test/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClientTest.java index 4636f1b2b..e8e7df206 100644 --- a/src/test/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClientTest.java +++ b/src/test/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftServiceClientTest.java @@ -13,7 +13,6 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -592,18 +591,34 @@ void testListTables() throws SQLException { } @Test - void testListTablesWithEmptyTypesReturnsEmptyWithoutServerCall() throws SQLException { - // Per JDBC spec: empty types array means "no types selected" → return no rows. - // The driver must short-circuit and NOT send the Thrift request to the server. + void testListTablesWithEmptyTypesMatchesAll() throws SQLException { + // Per the JDBC DatabaseMetaData.getTables contract, an empty types array carries no type + // constraint and must match ALL table types, identical to passing null. The driver must + // therefore query the server (with no table-type filter set on the request), not short-circuit. DatabricksThriftServiceClient client = new DatabricksThriftServiceClient(thriftAccessor, connectionContext); + when(session.getSessionInfo()).thenReturn(SESSION_INFO); + client.setServerProtocolVersion(TProtocolVersion.SPARK_CLI_SERVICE_PROTOCOL_V1); + + TFetchResultsResp response = + new TFetchResultsResp() + .setStatus(new TStatus().setStatusCode(TStatusCode.SUCCESS_STATUS)) + .setResults(resultData) + .setResultSetMetadata(resultMetadataData); + TColumn tColumn = new TColumn(); + tColumn.setStringVal(new TStringColumn().setValues(Collections.singletonList(""))); + when(resultData.getColumns()).thenReturn(List.of(tColumn, tColumn, tColumn, tColumn)); + when(thriftAccessor.getThriftResponse(any())).thenReturn(response); DatabricksResultSet resultSet = client.listTables(session, TEST_CATALOG, TEST_SCHEMA, TEST_TABLE, new String[0]); assertEquals(StatementState.SUCCEEDED, resultSet.getStatementStatus().getState()); - assertFalse(resultSet.next(), "Empty types array must yield zero rows"); - verify(thriftAccessor, never()).getThriftResponse(any()); + ArgumentCaptor captor = ArgumentCaptor.forClass(TGetTablesReq.class); + verify(thriftAccessor).getThriftResponse(captor.capture()); + assertFalse( + captor.getValue().isSetTableTypes(), + "Empty types array must not set a table-type filter on the request (match-all)"); } @Test diff --git a/src/test/java/com/databricks/jdbc/integration/e2e/MetadataTests.java b/src/test/java/com/databricks/jdbc/integration/e2e/MetadataTests.java index 308968ef2..909cc8bd3 100644 --- a/src/test/java/com/databricks/jdbc/integration/e2e/MetadataTests.java +++ b/src/test/java/com/databricks/jdbc/integration/e2e/MetadataTests.java @@ -153,6 +153,34 @@ void testTableInformation() throws SQLException { deleteTable(connection, tableName); } + @Test + void testGetTablesEmptyTypesMatchesAll() throws SQLException { + // Per the JDBC DatabaseMetaData.getTables contract, an empty types[] array carries no type + // constraint and must match ALL table types, identical to passing null. + DatabaseMetaData metaData = connection.getMetaData(); + String catalog = getDatabricksCatalog(); + String schema = getDatabricksSchema(); + String tableName = "empty_types_match_all_test_table"; + setupDatabaseTable(connection, tableName); + try { + // Sanity check: null types (match-all) lists the table. + try (ResultSet tablesNull = metaData.getTables(catalog, schema, tableName, null)) { + assertTrue( + tablesNull.next(), + "null types should match all table types and list the created table"); + } + // Empty types[] must behave identically to null (match-all), not match-none. + try (ResultSet tablesEmpty = + metaData.getTables(catalog, schema, tableName, new String[] {})) { + assertTrue( + tablesEmpty.next(), + "empty types[] should match all table types and list the created table"); + } + } finally { + deleteTable(connection, tableName); + } + } + @Test void testTableInformationExactMatch() throws SQLException { DatabaseMetaData metaData = connection.getMetaData();