diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index e12c8875f4..d6f09f3929 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -8,6 +8,9 @@ - `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 +- Invalid or incomplete Databricks JDBC URLs now fail with a descriptive `DatabricksSQLException` + instead of leaking a `NullPointerException` when required connection parameters are missing. + - 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/common/util/ValidationUtil.java b/src/main/java/com/databricks/jdbc/common/util/ValidationUtil.java index 4c29953100..d32f734e0f 100644 --- a/src/main/java/com/databricks/jdbc/common/util/ValidationUtil.java +++ b/src/main/java/com/databricks/jdbc/common/util/ValidationUtil.java @@ -148,6 +148,10 @@ public static void checkHTTPError(HttpResponse response) * @return true if the URL is valid, false otherwise */ public static boolean isValidJdbcUrl(String url) { + if (url == null) { + return false; + } + final List PATH_PATTERNS = List.of( HTTP_CLUSTER_PATH_PATTERN, @@ -176,11 +180,30 @@ public static boolean isValidJdbcUrl(String url) { */ public static void validateInputProperties(Map parameters) throws DatabricksValidationException { + validateRequiredConnectionParameters(parameters); // Fail fast on an unsupported AuthMech before the client-configurator machinery runs. validateAuthMech(parameters); validateUidParameter(parameters); } + /** + * Validates parameters that must be present in every connection configuration. URL parameters and + * {@link java.util.Properties} are merged before this method is called, so required values may be + * supplied through either mechanism. + * + * @param parameters merged JDBC connection parameters + * @throws DatabricksValidationException if any required parameter is missing or blank + */ + private static void validateRequiredConnectionParameters(Map parameters) + throws DatabricksValidationException { + String parameterName = DatabricksJdbcUrlParams.HTTP_PATH.getParamName().toLowerCase(); + String httpPath = parameters.get(parameterName); + if (httpPath == null || httpPath.isBlank()) { + throw new DatabricksValidationException( + "Missing required connection parameter: " + parameterName); + } + } + /** * Validates the AuthMech parameter. Reuses {@link AuthMech#fromValue} as the single source of * truth for supported values, so adding a new AuthMech only requires updating {@code AuthMech}. diff --git a/src/test/java/com/databricks/client/jdbc/DriverTest.java b/src/test/java/com/databricks/client/jdbc/DriverTest.java new file mode 100644 index 0000000000..d88b878080 --- /dev/null +++ b/src/test/java/com/databricks/client/jdbc/DriverTest.java @@ -0,0 +1,24 @@ +package com.databricks.client.jdbc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.databricks.jdbc.exception.DatabricksSQLException; +import java.util.Properties; +import org.junit.jupiter.api.Test; + +class DriverTest { + + @Test + void connectRejectsMissingRequiredConnectionParameters() { + DatabricksSQLException exception = + assertThrows( + DatabricksSQLException.class, + () -> + Driver.getInstance().connect("jdbc:databricks://localhost:8080", new Properties())); + + assertEquals("INPUT_VALIDATION_ERROR", exception.getSQLState()); + assertTrue(exception.getMessage().contains("httppath")); + } +} diff --git a/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java b/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java index 2d68683dec..7697f85034 100644 --- a/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java +++ b/src/test/java/com/databricks/jdbc/api/impl/DatabricksConnectionContextTest.java @@ -33,6 +33,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; class DatabricksConnectionContextTest { @@ -85,6 +86,26 @@ public void testParseInvalid() { assertThrows( DatabricksParsingException.class, () -> DatabricksConnectionContext.parse(TestConstants.INVALID_URL_2, properties)); + assertThrows( + DatabricksParsingException.class, + () -> DatabricksConnectionContext.parse(null, properties)); + } + + @ParameterizedTest + @ValueSource( + strings = { + "jdbc:databricks://localhost:8080", + "jdbc:databricks://localhost:8080;httpPath=", + "jdbc:databricks://localhost:8080;httpPath= " + }) + public void testParseRejectsMissingRequiredConnectionParameters(String url) { + DatabricksSQLException exception = + assertThrows( + DatabricksSQLException.class, + () -> DatabricksConnectionContext.parse(url, new Properties())); + + assertEquals("INPUT_VALIDATION_ERROR", exception.getSQLState()); + assertTrue(exception.getMessage().contains("httppath")); } @Test