This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new a00fc7dd10 [#13854] fix(jdbc): register the catalog's JDBC driver
before the create-time probe looks it up (#13855)
a00fc7dd10 is described below
commit a00fc7dd10305780c41847070828801d78d7e179
Author: Qi Yu <[email protected]>
AuthorDate: Thu Oct 15 14:07:27 2026 +0800
[#13854] fix(jdbc): register the catalog's JDBC driver before the
create-time probe looks it up (#13855)
### What changes were proposed in this pull request?
Before the create-time backend probe looks up its driver,
`JdbcCatalogOperations.probeConnection` now loads and initializes the
class named in `jdbc-driver`. It uses the catalog's class loader, so the
driver registers itself with `DriverManager` before `getDriver()` runs.
That class loader is also the one `DriverManager` checks the caller
against.
### Why are the changes needed?
When the driver ships only in the catalog's `libs` directory, as with
ClickHouse, the probe fails with `No suitable driver`. Such a driver
registers with `DriverManager` only when its class is initialized.
Nothing does that before the probe:
- The pool loads the driver lazily.
- `DataSourceUtils.verifyDriverPresent` loads the class without
initializing it.
Fix: #13854
### Does this PR introduce _any_ user-facing change?
No API or property change. Catalogs whose driver ships only in the
catalog libs can be created again with create-time verification enabled.
### How was this patch tested?
- New
`TestJdbcCatalogOperationsProbe#testProbeFindsDriverVisibleOnlyToCatalogClassLoader`.
It runs the probe from an isolated class loader against a driver that
self-registers on class initialization and has no `META-INF/services`
entry. Without the fix it fails with `No suitable driver`; with the fix
it passes.
- Unit tests pass for catalog-jdbc-common, catalog-jdbc-clickhouse,
catalog-jdbc-mysql and catalog-jdbc-postgresql with `-PskipITs`.
---------
Co-authored-by: yuqi1129 <[email protected]>
---
.../catalog/jdbc/JdbcCatalogOperations.java | 24 ++++
.../jdbc/TestJdbcCatalogOperationsProbe.java | 127 ++++++++++++++++++++-
2 files changed, 149 insertions(+), 2 deletions(-)
diff --git
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogOperations.java
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogOperations.java
index 7d771b7ac2..5b67a4aaf4 100644
---
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogOperations.java
+++
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogOperations.java
@@ -224,6 +224,29 @@ public class JdbcCatalogOperations
return DriverManager.getDriver(jdbcUrl);
}
+ /**
+ * Initializes the configured driver class so that it registers itself with
{@link DriverManager}
+ * before {@link #getDriver()} looks it up.
+ *
+ * <p>{@code DriverManager} only discovers drivers on the server classpath
by itself. A driver in
+ * the catalog's own libs directory registers only when its class is
initialized, which the lazy
+ * connection pool does on its first connection. The create-time probe runs
before that, so
+ * without this the lookup fails with "No suitable driver". The class is
loaded by this catalog's
+ * class loader, which is also the loader {@code DriverManager} checks the
caller against.
+ */
+ private void ensureDriverRegistered() {
+ String driverClassName = jdbcConfig.getJdbcDriver();
+ if (StringUtils.isBlank(driverClassName)) {
+ return;
+ }
+ try {
+ Class.forName(driverClassName, true, getClass().getClassLoader());
+ } catch (ClassNotFoundException e) {
+ // The data source already rejects a missing driver class when the
catalog is initialized;
+ // leave the lookup to getDriver(), which reports its own error.
+ }
+ }
+
/**
* Lists the schemas under the given namespace.
*
@@ -273,6 +296,7 @@ public class JdbcCatalogOperations
public void probeConnection(NameIdentifier catalogIdent) {
Driver driver;
try {
+ ensureDriverRegistered();
driver = getDriver();
} catch (SQLException e) {
throw new ConnectionFailedException(
diff --git
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperationsProbe.java
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperationsProbe.java
index 68cde50c87..d39bcbb386 100644
---
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperationsProbe.java
+++
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogOperationsProbe.java
@@ -18,15 +18,24 @@
*/
package org.apache.gravitino.catalog.jdbc;
+import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
+import java.net.URL;
+import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.Driver;
+import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.Properties;
+import java.util.concurrent.Callable;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
import
org.apache.gravitino.catalog.jdbc.converter.SqliteColumnDefaultValueConverter;
@@ -92,6 +101,34 @@ public class TestJdbcCatalogOperationsProbe {
}
}
+ /**
+ * A driver that lives only in the catalog's libs directory is invisible to
{@link DriverManager}
+ * until its class is initialized, and the lazy pool has not done that yet
when the create-time
+ * probe runs. The probe must still find it instead of failing with "No
suitable driver".
+ */
+ @Test
+ public void testProbeFindsDriverVisibleOnlyToCatalogClassLoader() throws
Exception {
+ List<URL> urls = new ArrayList<>();
+ for (String entry :
System.getProperty("java.class.path").split(File.pathSeparator)) {
+ urls.add(new File(entry).toURI().toURL());
+ }
+ ClassLoader original = Thread.currentThread().getContextClassLoader();
+ try (URLClassLoader catalogLoader =
+ new URLClassLoader(urls.toArray(new URL[0]),
ClassLoader.getPlatformClassLoader())) {
+ Thread.currentThread().setContextClassLoader(catalogLoader);
+ Class<?> probeClass = Class.forName(IsolatedProbe.class.getName(), true,
catalogLoader);
+ Assertions.assertNotSame(IsolatedProbe.class, probeClass);
+
+ @SuppressWarnings("unchecked")
+ Callable<String> probe = (Callable<String>)
probeClass.getDeclaredConstructor().newInstance();
+ String message = probe.call();
+
+ Assertions.assertTrue(message.contains(SelfRegisteringDriver.REACHED),
message);
+ } finally {
+ Thread.currentThread().setContextClassLoader(original);
+ }
+ }
+
@Test
public void testHostPortFromJdbcUrlNeverLeaksCredentials() {
// Plain host[:port] forms are returned.
@@ -196,8 +233,94 @@ public class TestJdbcCatalogOperationsProbe {
@Override
public java.util.logging.Logger getParentLogger() throws
SQLFeatureNotSupportedException {
- // This fake driver does not use java.util.logging. FQN is used because
java.sql.Driver
- // mandates java.util.logging.Logger, whose simple name would clash with
org.slf4j.Logger.
+ // This fake driver does not use java.util.logging. The FQN is required
because spotless
+ // rewrites every Logger import to org.slf4j.Logger.
+ throw new SQLFeatureNotSupportedException();
+ }
+ }
+
+ /**
+ * Runs the probe from inside the catalog class loader, the way a catalog
does in the server, and
+ * returns the connection-failure message.
+ */
+ public static class IsolatedProbe implements Callable<String> {
+ @Override
+ public String call() {
+ JdbcCatalogOperations ops =
+ new JdbcCatalogOperations(
+ new SqliteExceptionConverter(),
+ new SqliteTypeConverter(),
+ new SqliteDatabaseOperations(":memory:"),
+ new SqliteTableOperations(),
+ new SqliteColumnDefaultValueConverter());
+ Map<String, String> config = new HashMap<>();
+ config.put(JdbcConfig.JDBC_DRIVER.getKey(),
SelfRegisteringDriver.class.getName());
+ config.put(JdbcConfig.JDBC_URL.getKey(),
SelfRegisteringDriver.URL_PREFIX + "//db:1/test");
+ config.put(JdbcConfig.USERNAME.getKey(), "u");
+ config.put(JdbcConfig.PASSWORD.getKey(), "p");
+ ops.setProbeConfigForTest(new JdbcConfig(config));
+ ConnectionFailedException e =
+ Assertions.assertThrows(
+ ConnectionFailedException.class,
+ () -> ops.probeConnection(NameIdentifier.of("m", "c")));
+ return e.getMessage();
+ }
+ }
+
+ /**
+ * A driver that, like real JDBC drivers, registers itself with {@link
DriverManager} when its
+ * class is initialized. It has no {@code META-INF/services/java.sql.Driver}
entry, so nothing
+ * registers it automatically.
+ */
+ public static class SelfRegisteringDriver implements Driver {
+ static final String URL_PREFIX = "jdbc:selfregistering:";
+ static final String REACHED = "reached the catalog driver";
+
+ static {
+ try {
+ DriverManager.registerDriver(new SelfRegisteringDriver());
+ } catch (SQLException e) {
+ throw new ExceptionInInitializerError(e);
+ }
+ }
+
+ @Override
+ public Connection connect(String url, Properties info) throws SQLException
{
+ if (!acceptsURL(url)) {
+ return null;
+ }
+ throw new SQLException(REACHED);
+ }
+
+ @Override
+ public boolean acceptsURL(String url) {
+ return url != null && url.startsWith(URL_PREFIX);
+ }
+
+ @Override
+ public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
+ return new DriverPropertyInfo[0];
+ }
+
+ @Override
+ public int getMajorVersion() {
+ return 1;
+ }
+
+ @Override
+ public int getMinorVersion() {
+ return 0;
+ }
+
+ @Override
+ public boolean jdbcCompliant() {
+ return false;
+ }
+
+ @Override
+ public java.util.logging.Logger getParentLogger() throws
SQLFeatureNotSupportedException {
+ // This fake driver does not use java.util.logging. The FQN is required
because spotless
+ // rewrites every Logger import to org.slf4j.Logger.
throw new SQLFeatureNotSupportedException();
}
}