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

jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new 07caed0ae4 [Cherry-pick to branch-1.3] [#12938] fix(jdbc): Resolve 
PostgreSQL database names before accepting catalogs (#12939) (#12952)
07caed0ae4 is described below

commit 07caed0ae4fa0aa14f01555e78b8ca7b2140ac0e
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Mon Sep 7 20:21:30 2026 +0800

    [Cherry-pick to branch-1.3] [#12938] fix(jdbc): Resolve PostgreSQL database 
names before accepting catalogs (#12939) (#12952)
    
    **Cherry-pick Information:**
    - Original commit: 41df9618176b344c686b779bef953be7a633f6a8
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: Qi Yu <[email protected]>
---
 .../apache/gravitino/catalog/jdbc/JdbcCatalog.java |  34 ++++
 .../gravitino/catalog/jdbc/config/JdbcConfig.java  |   4 +-
 catalogs/catalog-jdbc-postgresql/build.gradle.kts  |   1 +
 .../catalog/postgresql/PostgreSqlCatalog.java      |  18 +++
 .../src/main/resources/jdbc-postgresql.conf        |   1 +
 .../TestPostgreSqlCatalogConfiguration.java        | 171 +++++++++++++++++++++
 docs/jdbc-postgresql-catalog.md                    |   8 +-
 7 files changed, 232 insertions(+), 5 deletions(-)

diff --git 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalog.java
 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalog.java
index 55f4b957ed..65bf8eb897 100644
--- 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalog.java
+++ 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalog.java
@@ -18,9 +18,12 @@
  */
 package org.apache.gravitino.catalog.jdbc;
 
+import com.google.common.base.Preconditions;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
 import 
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
@@ -118,6 +121,37 @@ public abstract class JdbcCatalog extends 
BaseCatalog<JdbcCatalog> {
     return TABLE_PROPERTIES_META;
   }
 
+  /**
+   * Resolves the database required by a JDBC provider before accepting its 
configuration.
+   *
+   * @param config The catalog configuration.
+   * @param databaseFromUrl A driver-backed parser returning the database, or 
null if absent, and
+   *     rejecting invalid URLs.
+   * @return A copy of the configuration containing a nonblank database.
+   * @throws IllegalArgumentException if the URL is invalid, no database is 
configured, or the
+   *     database names conflict.
+   */
+  protected static Map<String, String> resolveJdbcDatabase(
+      Map<String, String> config, Function<String, String> databaseFromUrl) {
+    String database = config.get(JdbcConfig.JDBC_DATABASE.getKey());
+    Preconditions.checkArgument(
+        !config.containsKey(JdbcConfig.JDBC_DATABASE.getKey()) || 
StringUtils.isNotBlank(database),
+        "jdbc-database must be nonblank when explicitly configured");
+    String urlDatabase = databaseFromUrl.apply(new 
JdbcConfig(config).getJdbcUrl());
+    if (!config.containsKey(JdbcConfig.JDBC_DATABASE.getKey())) {
+      database = urlDatabase;
+    }
+    Preconditions.checkArgument(
+        StringUtils.isNotBlank(database),
+        "jdbc-database must be nonblank; specify it explicitly or include the 
database in jdbc-url");
+    Preconditions.checkArgument(
+        StringUtils.isBlank(urlDatabase) || database.equals(urlDatabase),
+        "jdbc-database must match the database specified in jdbc-url");
+    Map<String, String> resolved = new HashMap<>(config);
+    resolved.put(JdbcConfig.JDBC_DATABASE.getKey(), database);
+    return resolved;
+  }
+
   @Override
   protected void addCatalogSpecificCredentialProviders(
       Map<String, String> properties, List<String> credentialProviders) {
diff --git 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/config/JdbcConfig.java
 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/config/JdbcConfig.java
index 18f1023b20..e474bcda39 100644
--- 
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/config/JdbcConfig.java
+++ 
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/config/JdbcConfig.java
@@ -39,7 +39,9 @@ public class JdbcConfig extends Config {
 
   public static final ConfigEntry<String> JDBC_DATABASE =
       new ConfigBuilder("jdbc-database")
-          .doc("The database of the jdbc connection")
+          .doc(
+              "The database of the JDBC connection. PostgreSQL requires a 
nonblank "
+                  + "value here or a database in jdbc-url; other JDBC 
providers do not require it.")
           .version(ConfigConstants.VERSION_0_3_0)
           .stringConf()
           .checkValue(StringUtils::isNotBlank, 
ConfigConstants.NOT_BLANK_ERROR_MSG)
diff --git a/catalogs/catalog-jdbc-postgresql/build.gradle.kts 
b/catalogs/catalog-jdbc-postgresql/build.gradle.kts
index 16c4d2dca3..a394596ea4 100644
--- a/catalogs/catalog-jdbc-postgresql/build.gradle.kts
+++ b/catalogs/catalog-jdbc-postgresql/build.gradle.kts
@@ -28,6 +28,7 @@ dependencies {
   compileOnly(project(":api"))
   compileOnly(project(":common"))
   compileOnly(project(":core"))
+  compileOnly(libs.postgresql.driver)
 
   implementation(project(":catalogs:catalog-jdbc-common")) {
     exclude(group = "*")
diff --git 
a/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/PostgreSqlCatalog.java
 
b/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/PostgreSqlCatalog.java
index e7d9b12140..ca8ec349ef 100644
--- 
a/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/PostgreSqlCatalog.java
+++ 
b/catalogs/catalog-jdbc-postgresql/src/main/java/org/apache/gravitino/catalog/postgresql/PostgreSqlCatalog.java
@@ -18,7 +18,10 @@
  */
 package org.apache.gravitino.catalog.postgresql;
 
+import com.google.common.base.Preconditions;
 import java.util.Map;
+import java.util.Properties;
+import javax.annotation.Nullable;
 import org.apache.gravitino.catalog.jdbc.JdbcCatalog;
 import 
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
 import org.apache.gravitino.catalog.jdbc.converter.JdbcExceptionConverter;
@@ -32,9 +35,17 @@ import 
org.apache.gravitino.catalog.postgresql.operation.PostgreSqlSchemaOperati
 import 
org.apache.gravitino.catalog.postgresql.operation.PostgreSqlTableOperations;
 import org.apache.gravitino.connector.CatalogOperations;
 import org.apache.gravitino.connector.capability.Capability;
+import org.postgresql.Driver;
+import org.postgresql.PGProperty;
 
 public class PostgreSqlCatalog extends JdbcCatalog {
 
+  /** {@inheritDoc} */
+  @Override
+  public JdbcCatalog withCatalogConf(Map<String, String> conf) {
+    return super.withCatalogConf(resolveJdbcDatabase(conf, 
PostgreSqlCatalog::databaseFromUrl));
+  }
+
   @Override
   public String shortName() {
     return "jdbc-postgresql";
@@ -80,4 +91,11 @@ public class PostgreSqlCatalog extends JdbcCatalog {
   protected JdbcColumnDefaultValueConverter 
createJdbcColumnDefaultValueConverter() {
     return new PostgreSqlColumnDefaultValueConverter();
   }
+
+  @Nullable
+  private static String databaseFromUrl(String url) {
+    Properties parsed = Driver.parseURL(url, new Properties());
+    Preconditions.checkArgument(parsed != null, "Invalid PostgreSQL jdbc-url");
+    return parsed.getProperty(PGProperty.PG_DBNAME.getName());
+  }
 }
diff --git 
a/catalogs/catalog-jdbc-postgresql/src/main/resources/jdbc-postgresql.conf 
b/catalogs/catalog-jdbc-postgresql/src/main/resources/jdbc-postgresql.conf
index 94a9ce8eaa..cde703010e 100644
--- a/catalogs/catalog-jdbc-postgresql/src/main/resources/jdbc-postgresql.conf
+++ b/catalogs/catalog-jdbc-postgresql/src/main/resources/jdbc-postgresql.conf
@@ -19,5 +19,6 @@
 # jdbc-url = jdbc:postgresql://localhost:5432/your_database
 # jdbc-user = strato
 # jdbc-password = strato
+# jdbc-database is optional when the database is specified in jdbc-url.
 # jdbc-database = your_database
 # jdbc-driver = org.postgresql.Driver
\ No newline at end of file
diff --git 
a/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/TestPostgreSqlCatalogConfiguration.java
 
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/TestPostgreSqlCatalogConfiguration.java
new file mode 100644
index 0000000000..ef1e83047e
--- /dev/null
+++ 
b/catalogs/catalog-jdbc-postgresql/src/test/java/org/apache/gravitino/catalog/postgresql/TestPostgreSqlCatalogConfiguration.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.catalog.postgresql;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.connector.CatalogOperations;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.CatalogEntity;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/** Tests database resolution before catalog operations are initialized. */
+class TestPostgreSqlCatalogConfiguration {
+
+  @ParameterizedTest
+  @CsvSource(
+      value = {
+        "jdbc:postgresql://localhost:5432/demo|demo",
+        "jdbc:postgresql:demo|demo",
+        "jdbc:postgresql://host1:5432,host2:5432/demo?ssl=true|demo",
+        "jdbc:postgresql://localhost/my%20db|my db",
+        "jdbc:postgresql://localhost/demo?currentSchema=other|demo",
+        "jdbc:postgresql://localhost/demo?PGDBNAME=other|other"
+      },
+      delimiter = '|')
+  void testDatabaseFromUrl(String url, String database) {
+    Map<String, String> config = Map.of("jdbc-url", url);
+    Map<String, String> resolved = captureOperationsConfig(config);
+    Assertions.assertEquals(database, resolved.get("jdbc-database"));
+    Assertions.assertEquals(url, resolved.get("jdbc-url"));
+    Assertions.assertFalse(config.containsKey("jdbc-database"));
+  }
+
+  @ParameterizedTest
+  @CsvSource(
+      value = {
+        "jdbc:postgresql://localhost:5432/demo|demo",
+        "jdbc:postgresql://localhost/my%20db|my db",
+        "jdbc:postgresql://localhost/|explicit"
+      },
+      delimiter = '|')
+  void testExplicitDatabase(String url, String database) {
+    Map<String, String> config = Map.of("jdbc-url", url, "jdbc-database", 
database);
+    Assertions.assertEquals(config, captureOperationsConfig(config));
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "jdbc:postgresql://localhost/demo",
+        "jdbc:postgresql://localhost/demo?PGDBNAME=other"
+      })
+  void testConflictingDatabaseRejected(String url) {
+    IllegalArgumentException error =
+        Assertions.assertThrows(
+            IllegalArgumentException.class,
+            () ->
+                new PostgreSqlCatalog()
+                    .withCatalogConf(Map.of("jdbc-url", url, "jdbc-database", 
"conflicting")));
+    Assertions.assertEquals(
+        "jdbc-database must match the database specified in jdbc-url", 
error.getMessage());
+  }
+
+  @ParameterizedTest
+  @NullAndEmptySource
+  @ValueSource(strings = {" ", "\t"})
+  void testBlankExplicitDatabaseRejected(String database) {
+    Map<String, String> config = new HashMap<>();
+    config.put("jdbc-url", "jdbc:postgresql://localhost:5432/demo");
+    config.put("jdbc-database", database);
+    IllegalArgumentException error =
+        Assertions.assertThrows(
+            IllegalArgumentException.class, () -> new 
PostgreSqlCatalog().withCatalogConf(config));
+    Assertions.assertTrue(error.getMessage().contains("jdbc-database"));
+  }
+
+  @Test
+  void testMissingDatabaseRejectedBeforeFirstOperation() {
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            new PostgreSqlCatalog()
+                .withCatalogConf(Map.of("jdbc-url", 
"jdbc:postgresql://localhost/")));
+  }
+
+  @ParameterizedTest
+  @CsvSource(
+      value = {
+        "invalid|",
+        "invalid|demo",
+        "jdbc:mysql://localhost/demo|",
+        "jdbc:mysql://localhost/demo|demo",
+        "jdbc:postgresql://localhost:invalid/demo|",
+        "jdbc:postgresql://localhost:invalid/demo|demo",
+        "jdbc:postgresql://localhost:5432|",
+        "jdbc:postgresql://localhost:5432|demo"
+      },
+      delimiter = '|')
+  void testInvalidUrlRejected(String url, String database) {
+    Map<String, String> config = new HashMap<>();
+    config.put("jdbc-url", url);
+    if (database != null) {
+      config.put("jdbc-database", database);
+    }
+    IllegalArgumentException error =
+        Assertions.assertThrows(
+            IllegalArgumentException.class, () -> new 
PostgreSqlCatalog().withCatalogConf(config));
+    Assertions.assertEquals("Invalid PostgreSQL jdbc-url", error.getMessage());
+  }
+
+  @Test
+  void testMissingUrlRejected() {
+    Assertions.assertThrows(
+        IllegalArgumentException.class, () -> new 
PostgreSqlCatalog().withCatalogConf(Map.of()));
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () -> new PostgreSqlCatalog().withCatalogConf(Map.of("jdbc-database", 
"demo")));
+  }
+
+  private static Map<String, String> captureOperationsConfig(Map<String, 
String> config) {
+    CapturingCatalog catalog = new CapturingCatalog();
+    catalog.withCatalogConf(config);
+    catalog.withCatalogEntity(
+        CatalogEntity.builder()
+            .withId(1L)
+            .withName("test")
+            .withNamespace(Namespace.of("metalake"))
+            .withType(Catalog.Type.RELATIONAL)
+            .withProvider(catalog.shortName())
+            .withAuditInfo(
+                
AuditInfo.builder().withCreator("test").withCreateTime(Instant.EPOCH).build())
+            .build());
+    Assertions.assertThrows(UnsupportedOperationException.class, catalog::ops);
+    return catalog.config;
+  }
+
+  private static class CapturingCatalog extends PostgreSqlCatalog {
+    private Map<String, String> config;
+
+    /** {@inheritDoc} */
+    @Override
+    protected CatalogOperations newOps(Map<String, String> conf) {
+      config = conf;
+      throw new UnsupportedOperationException("Configuration captured without 
connecting");
+    }
+  }
+}
diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md
index 28fa2b2c8b..ae0dd04ebc 100644
--- a/docs/jdbc-postgresql-catalog.md
+++ b/docs/jdbc-postgresql-catalog.md
@@ -36,14 +36,14 @@ Check the relevant data source configuration in [data 
source properties](https:/
 
 When using Gravitino with Trino, pass the Trino PostgreSQL connector 
configuration using the `trino.bypass.` prefix. For example, using 
`trino.bypass.join-pushdown.strategy` to pass the `join-pushdown.strategy` to 
the Gravitino PostgreSQL catalog in Trino runtime.
 
-If you use JDBC catalog, you must provide `jdbc-url`, `jdbc-driver`, 
`jdbc-database`, `jdbc-user` and `jdbc-password` to catalog properties.
+If you use JDBC catalog, you must provide `jdbc-url`, `jdbc-driver`, 
`jdbc-user` and `jdbc-password` to catalog properties.
 Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-properties-configuration), the 
PostgreSQL catalog has the following properties:
 
 | Configuration item      | Description                                        
                                                                                
                               | Default value | Required |
 
|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|
-| `jdbc-url`              | JDBC URL for connecting to the database. You need 
to specify the database in the URL. For example 
`jdbc:postgresql://localhost:3306/pg_database?sslmode=require`. | (none)        
| Yes      |
+| `jdbc-url`              | A valid PostgreSQL JDBC URL, for example 
`jdbc:postgresql://localhost:5432/pg_database?sslmode=require`. If the URL 
omits the database, set `jdbc-database`. | (none)        | Yes      |
 | `jdbc-driver`           | The driver of the JDBC connection. For example 
`org.postgresql.Driver`.                                                        
                                   | (none)        | Yes      |
-| `jdbc-database`         | The database of the JDBC connection. Configure it 
with the same value as the database in the `jdbc-url`. For example 
`pg_database`.                               | (none)        | Yes      |
+| `jdbc-database`         | The database of the JDBC connection. Derived from 
`jdbc-url` when omitted. An explicit value must be nonblank and match the URL 
database when both are provided. | (none)        | Only if absent from 
`jdbc-url` |
 | `jdbc-user`             | The JDBC user name.                                
                                                                                
                               | (none)        | Yes      |
 | `jdbc-password`         | The JDBC password.                                 
                                                                                
                               | (none)        | Yes      |
 | `jdbc.pool.min-size`    | The minimum number of connections in the pool. `2` 
by default.                                                                     
                               | `2`           | No       |
@@ -52,7 +52,7 @@ Besides the [common catalog 
properties](./gravitino-server-config.md#catalog-pro
 
 :::caution
 Download the corresponding JDBC driver to the `catalogs/jdbc-postgresql/libs` 
directory.
-Explicitly specify the database in both `jdbc-url` and `jdbc-database`. An 
error may occur if the values in both aren't consistent.
+When `jdbc-database` is omitted, the catalog derives it from `jdbc-url`. 
Catalog creation rejects invalid PostgreSQL URLs, even when `jdbc-database` is 
set, and fails if neither provides a nonblank database name. When both 
`jdbc-database` and the URL specify a database, their names must match; catalog 
creation rejects conflicting values.
 :::
 :::info
 In PostgreSQL, the database corresponds to the Gravitino catalog, and the 
schema corresponds to the Gravitino schema.

Reply via email to