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.