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();
     }
   }

Reply via email to