This is an automated email from the ASF dual-hosted git repository.

diqiu50 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 e210811550 [#13192] improvement(jdbc-catalog): fail fast with a clear 
error when the JDBC driver is missing (#13193)
e210811550 is described below

commit e210811550cee04737464debf694848b732176ca
Author: geyanggang <[email protected]>
AuthorDate: Wed Sep 16 10:40:13 2026 +0800

    [#13192] improvement(jdbc-catalog): fail fast with a clear error when the 
JDBC driver is missing (#13193)
    
    ### What changes were proposed in this pull request?
    
    - `DataSourceUtils`: verify the configured `jdbc-driver` class is
    loadable from
    the catalog classpath (via `Class.forName`, without initialization)
    before the
    DBCP2 pool is created. A missing driver now fails at catalog creation
    with a
    clear message naming the driver, instead of an opaque
    `ClassNotFoundException`
      surfaced lazily on the first connection.
    - Added a unit test (`testMissingDriverGivesClearError`).
    - Docs: clarified the MySQL and OceanBase catalog pages — which driver
    artifact/version to obtain, where to place it, and (for OceanBase) that
    either
      the OceanBase or MySQL Connector/J driver works.
    
    ### Why are the changes needed?
    
    Gravitino does not package these drivers; users install them themselves.
    Without
    this, a missing driver produces a confusing, wrapped
    `ClassNotFoundException`
    late in the flow and never names the driver to install.
    
    Fix: #13192
    
    ### Does this PR introduce any user-facing change?
    
    Yes, a clearer error message when a JDBC catalog's driver is not
    installed, and
    updated MySQL/OceanBase docs. No behavior change when the driver is
    present.
    
    ### How was this patch tested?
    
    `:catalogs:catalog-jdbc-common:test` (22 tests, incl. the new one) and
    `spotlessJavaCheck` pass.
---
 .../catalog/jdbc/utils/DataSourceUtils.java        | 57 ++++++++++++++++++++++
 .../jdbc/utils/TestDataSourceUrlValidation.java    | 25 ++++++++++
 docs/jdbc-mysql-catalog.md                         | 13 ++++-
 docs/jdbc-oceanbase-catalog.md                     | 16 +++++-
 4 files changed, 108 insertions(+), 3 deletions(-)

diff --git 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java
 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java
index e06f376c7b..b585464a0f 100644
--- 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java
+++ 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/utils/DataSourceUtils.java
@@ -90,10 +90,45 @@ public class DataSourceUtils {
     try {
       return createDBCPDataSource(jdbcConfig);
     } catch (Exception exception) {
+      if (isDriverClassMissing(exception)) {
+        // Some JDBC drivers are not packaged with Gravitino and must be 
installed by the user.
+        // Surface a clear, actionable message naming the driver instead of a 
raw
+        // ClassNotFoundException.
+        throw new GravitinoRuntimeException(
+            exception,
+            "JDBC driver class '%s' was not found on the catalog classpath. 
Install the driver "
+                + "JAR in the catalog's libs directory and recreate the 
catalog.",
+            jdbcConfig.getJdbcDriver());
+      }
       throw new GravitinoRuntimeException(exception, "Error creating 
datasource");
     }
   }
 
+  /**
+   * Returns whether the given throwable chain indicates a missing JDBC driver 
class. DBCP2 reports
+   * an absent driver as a {@link ClassNotFoundException} (sometimes wrapped 
in a {@link
+   * SQLException} whose message is "Cannot load JDBC driver class ..."), so 
both the exception
+   * chain and that message are checked.
+   *
+   * @param throwable the throwable thrown while creating the data source
+   * @return {@code true} if the failure is due to a driver class that cannot 
be loaded
+   */
+  private static boolean isDriverClassMissing(Throwable throwable) {
+    for (Throwable current = throwable; current != null; current = 
current.getCause()) {
+      if (current instanceof ClassNotFoundException || current instanceof 
NoClassDefFoundError) {
+        return true;
+      }
+      String message = current.getMessage();
+      if (message != null && message.contains("Cannot load JDBC driver 
class")) {
+        return true;
+      }
+      if (current.getCause() == current) {
+        break;
+      }
+    }
+    return false;
+  }
+
   private static DataSource createDBCPDataSource(JdbcConfig jdbcConfig) throws 
Exception {
     JdbcUrlUtils.validateJdbcConfig(
         jdbcConfig.getJdbcDriver(), jdbcConfig.getJdbcUrl(), 
jdbcConfig.getAllConfig());
@@ -102,6 +137,11 @@ public class DataSourceUtils {
     String jdbcUrl = jdbcConfig.getJdbcUrl();
     basicDataSource.setUrl(jdbcUrl);
     String driverClassName = jdbcConfig.getJdbcDriver();
+    // DBCP2 loads the driver lazily on the first connection, so a missing 
driver would otherwise
+    // only surface much later (and as an opaque error). Verify the driver 
class is on the catalog
+    // classpath now, so catalog creation fails fast with a clear, actionable 
message. Loaded
+    // without initialization; the H2 driver is already rejected above.
+    verifyDriverPresent(driverClassName);
     basicDataSource.setDriverClassName(driverClassName);
     String userName = jdbcConfig.getUsername();
     basicDataSource.setUsername(userName);
@@ -117,6 +157,23 @@ public class DataSourceUtils {
     return basicDataSource;
   }
 
+  /**
+   * Verifies that the configured JDBC driver class is loadable from the 
catalog classpath. The
+   * class is resolved without initialization using the catalog's context 
class loader (falling back
+   * to this class's loader), so an absent driver fails catalog creation 
immediately rather than on
+   * the first connection.
+   *
+   * @param driverClassName the fully qualified JDBC driver class name
+   * @throws ClassNotFoundException if the driver class is not on the catalog 
classpath
+   */
+  private static void verifyDriverPresent(String driverClassName) throws 
ClassNotFoundException {
+    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+    if (classLoader == null) {
+      classLoader = DataSourceUtils.class.getClassLoader();
+    }
+    Class.forName(driverClassName, false, classLoader);
+  }
+
   private static Properties getProperties(JdbcConfig jdbcConfig) {
     Properties properties = new Properties();
     properties.putAll(jdbcConfig.getAllConfig());
diff --git 
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/utils/TestDataSourceUrlValidation.java
 
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/utils/TestDataSourceUrlValidation.java
index dc3cffbc50..128c3c1ade 100644
--- 
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/utils/TestDataSourceUrlValidation.java
+++ 
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/utils/TestDataSourceUrlValidation.java
@@ -422,4 +422,29 @@ public class TestDataSourceUrlValidation {
       dataSource.close();
     }
   }
+
+  @Test
+  public void testMissingDriverGivesClearError() {
+    // A driver that is not on the classpath must fail fast at catalog 
creation with an actionable
+    // message naming the driver, not a raw ClassNotFoundException surfaced 
later on first use.
+    // Use a driver class that is guaranteed absent so the assertion does not 
depend on which
+    // vendor jars happen to be present.
+    String missingDriver = "com.example.NonExistentJdbcDriver";
+    HashMap<String, String> properties = Maps.newHashMap();
+    properties.put(JdbcConfig.JDBC_DRIVER.getKey(), missingDriver);
+    properties.put(JdbcConfig.JDBC_URL.getKey(), "jdbc:sqlite::memory:");
+    properties.put(JdbcConfig.USERNAME.getKey(), "test");
+    properties.put(JdbcConfig.PASSWORD.getKey(), "test");
+
+    GravitinoRuntimeException gre =
+        Assertions.assertThrows(
+            GravitinoRuntimeException.class, () -> 
DataSourceUtils.createDataSource(properties));
+    Assertions.assertTrue(
+        gre.getMessage().contains(missingDriver),
+        "message should name the missing driver: " + gre.getMessage());
+    Assertions.assertTrue(
+        gre.getMessage().contains("was not found"),
+        "message should state the driver was not found: " + gre.getMessage());
+    Assertions.assertInstanceOf(ClassNotFoundException.class, gre.getCause());
+  }
 }
diff --git a/docs/jdbc-mysql-catalog.md b/docs/jdbc-mysql-catalog.md
index 1969d7cb51..0e8aa5dbe9 100644
--- a/docs/jdbc-mysql-catalog.md
+++ b/docs/jdbc-mysql-catalog.md
@@ -52,7 +52,18 @@ Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-pro
 | `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.      | `30000`       | No       |
 
 :::caution
-Download the corresponding JDBC driver to the `catalogs/jdbc-mysql/libs` 
directory.
+Gravitino does not package the MySQL JDBC driver, because MySQL Connector/J is
+GPLv2 and cannot be redistributed. You must supply it yourself.
+
+Download MySQL Connector/J (`com.mysql:mysql-connector-j`, 8.0.16 or later; the
+`com.mysql.cj.jdbc.Driver` class) from
+[Maven Central](https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/) 
and
+place the JAR in the `catalogs/jdbc-mysql/libs` directory.
+
+For container or Kubernetes deployments where you cannot copy into that
+directory directly, supply the driver through your deployment's mechanism for
+adding catalog libraries. The catalog fails fast at creation time with a clear
+message if the driver is absent.
 :::
 
 ### Driver Version Compatibility
diff --git a/docs/jdbc-oceanbase-catalog.md b/docs/jdbc-oceanbase-catalog.md
index 95e6d7e545..61ebf2e0ec 100644
--- a/docs/jdbc-oceanbase-catalog.md
+++ b/docs/jdbc-oceanbase-catalog.md
@@ -50,8 +50,20 @@ Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-pro
 | `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a 
connection to be returned. `30000` by default.                                  
   | `30000`       | No       |
 
 :::caution
-Before using the OceanBase Catalog, you must download the corresponding JDBC 
driver to the `catalogs/jdbc-oceanbase/libs` directory.
-Gravitino doesn't package the JDBC driver for OceanBase due to licensing 
issues.
+Gravitino does not package the OceanBase JDBC driver due to licensing, so you
+must supply it yourself. OceanBase speaks the MySQL wire protocol, so either
+driver works: OceanBase Connector/J (`com.oceanbase:oceanbase-client`, 2.4.18
+or later; the `com.oceanbase.jdbc.Driver` class) or MySQL Connector/J
+(`com.mysql:mysql-connector-j`, 8.0.16 or later; the `com.mysql.cj.jdbc.Driver`
+class). Download it from Maven Central
+([OceanBase](https://repo1.maven.org/maven2/com/oceanbase/oceanbase-client/),
+[MySQL](https://repo1.maven.org/maven2/com/mysql/mysql-connector-j/)) and place
+the JAR in the `catalogs/jdbc-oceanbase/libs` directory.
+
+For container or Kubernetes deployments where you cannot copy into that
+directory directly, supply the driver through your deployment's mechanism for
+adding catalog libraries. The catalog fails fast at creation time with a clear
+message if the driver is absent.
 :::
 
 ### Driver Version Compatibility

Reply via email to