This is an automated email from the ASF dual-hosted git repository.
yuqi1129 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 7a58dd7132 [Cherry-pick to branch-1.3] [#13945] fix(jdbc): Honor pool
wait settings and validate MySQL table properties (#13946) (#14035)
7a58dd7132 is described below
commit 7a58dd7132ff121a993481d8b0d2169e8c8cfab7
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Oct 21 14:37:52 2026 +0800
[Cherry-pick to branch-1.3] [#13945] fix(jdbc): Honor pool wait settings
and validate MySQL table properties (#13946) (#14035)
### What changes were proposed in this pull request?
Backport commit `3ce91977a2d137af39321716a9fb5813922afceb` (#13946) to
`branch-1.3`: honor JDBC pool wait settings and validate MySQL table
properties.
Resolve the committed conflict markers while retaining the 1.3
validation query, connection timeout handling and Spark
`getPGInstance()` API. Adapt tests to the existing 1.3 ALTER connection
lifecycle. Pool max-idle and table credential-vending settings are not
introduced by this backport.
### Why are the changes needed?
Configured pool wait values could be lost or overwritten, and
unsupported MySQL table options could reach generated SQL. This backport
brings the fixes to 1.3.
### Does this PR introduce _any_ user-facing change?
Yes. JDBC pool wait settings are honored, and unsupported or invalid
MySQL table options are rejected before DDL is executed.
### How was this patch tested?
- 70 targeted non-Docker tests passed across JDBC common, MySQL, Flink
common and Spark common.
- Formatting and conflict-marker checks passed.
- Six mutation cases fail if the retained `SELECT 1` pool validation
query is removed, then pass after restoration.
- Docker tests were not run locally because the Docker daemon is
unavailable; CI validation is pending.
---------
Co-authored-by: Qi Yu <[email protected]>
---
.../jdbc/JdbcCatalogPropertiesMetadata.java | 7 +
.../catalog/jdbc/utils/DataSourceUtils.java | 7 +-
.../jdbc/TestJdbcCatalogPropertiesMetadata.java | 91 +++++++++
.../mysql/MysqlTablePropertiesMetadata.java | 34 +++-
.../mysql/operation/MysqlTableOperations.java | 22 ++-
.../mysql/TestMysqlTablePropertiesMetadata.java | 214 +++++++++++++++++++++
.../mysql/operation/TestMysqlTableOperations.java | 11 +-
.../TestMysqlTableOperationsSqlGeneration.java | 92 +++++++++
docs/jdbc-clickhouse-catalog.md | 2 +-
docs/jdbc-doris-catalog.md | 2 +-
docs/jdbc-mysql-catalog.md | 6 +-
docs/jdbc-oceanbase-catalog.md | 2 +-
docs/jdbc-postgresql-catalog.md | 2 +-
docs/jdbc-starrocks-catalog.md | 2 +-
.../connector/jdbc/JdbcPropertiesConverter.java | 21 ++
.../connector/integration/test/FlinkCommonIT.java | 35 ++--
.../test/jdbc/FlinkJdbcMysqlCatalogIT.java | 32 +++
.../AbstractJdbcPropertiesConverterTestSuite.java | 21 ++
.../connector/jdbc/JdbcPropertiesConverter.java | 6 +-
.../jdbc/TestJdbcPropertiesConverter.java | 42 ++++
20 files changed, 611 insertions(+), 40 deletions(-)
diff --git
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogPropertiesMetadata.java
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogPropertiesMetadata.java
index 5f6c84ba84..5dc879452e 100644
---
a/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogPropertiesMetadata.java
+++
b/catalogs/catalog-jdbc-common/src/main/java/org/apache/gravitino/catalog/jdbc/JdbcCatalogPropertiesMetadata.java
@@ -46,6 +46,7 @@ public class JdbcCatalogPropertiesMetadata extends
BaseCatalogPropertiesMetadata
JdbcConfig.PASSWORD.getKey(),
JdbcConfig.POOL_MIN_SIZE.getKey(),
JdbcConfig.POOL_MAX_SIZE.getKey(),
+ JdbcConfig.POOL_MAX_WAIT_MS.getKey(),
JdbcConfig.TEST_ON_BORROW.getKey(),
JdbcConfig.CONNECT_TIMEOUT_MS.getKey(),
JdbcConfig.VERIFY_ON_CREATE.getKey());
@@ -107,6 +108,12 @@ public class JdbcCatalogPropertiesMetadata extends
BaseCatalogPropertiesMetadata
JdbcConfig.POOL_MAX_SIZE.getDefaultValue(),
false /* hidden */,
false /* reserved */),
+ longOptionalPropertyEntry(
+ JdbcConfig.POOL_MAX_WAIT_MS.getKey(),
+ JdbcConfig.POOL_MAX_WAIT_MS.getDoc(),
+ false /* immutable */,
+ JdbcConfig.POOL_MAX_WAIT_MS.getDefaultValue(),
+ false /* hidden */),
booleanPropertyEntry(
JdbcConfig.TEST_ON_BORROW.getKey(),
JdbcConfig.TEST_ON_BORROW.getDoc(),
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 1430cf4c8c..c298fbd4d1 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
@@ -122,7 +122,12 @@ public class DataSourceUtils {
// executed to confirm whether the connection is valid.
basicDataSource.setTestOnBorrow(jdbcConfig.getTestOnBorrow());
basicDataSource.setValidationQuery(POOL_TEST_QUERY);
- basicDataSource.setMaxWait(Duration.ofMillis(jdbcConfig.getMaxWaitMs()));
+ // Keep a maxWaitMillis supplied through the DBCP bypass unless the
canonical setting is
+ // explicitly configured.
+ if
(jdbcConfig.getAllConfig().containsKey(JdbcConfig.POOL_MAX_WAIT_MS.getKey())
+ || !jdbcConfig.getAllConfig().containsKey("maxWaitMillis")) {
+ basicDataSource.setMaxWait(Duration.ofMillis(jdbcConfig.getMaxWaitMs()));
+ }
// Apply the driver-aware connect timeout as a DBCP connection property
(not a pool key, so the
// pool stays lazy). It bounds a single connection and takes effect on the
first real connection
// — i.e. create-time verification — so an unreachable backend fails fast
instead of hanging.
diff --git
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogPropertiesMetadata.java
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogPropertiesMetadata.java
index d810dd1d72..a77a6be771 100644
---
a/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogPropertiesMetadata.java
+++
b/catalogs/catalog-jdbc-common/src/test/java/org/apache/gravitino/catalog/jdbc/TestJdbcCatalogPropertiesMetadata.java
@@ -20,22 +20,31 @@ package org.apache.gravitino.catalog.jdbc;
import com.google.common.collect.ImmutableMap;
import java.time.Instant;
+import java.util.HashMap;
import java.util.Map;
+import org.apache.commons.dbcp2.BasicDataSource;
+import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Catalog;
import org.apache.gravitino.Namespace;
import org.apache.gravitino.catalog.jdbc.config.JdbcConfig;
import
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
import org.apache.gravitino.catalog.jdbc.converter.JdbcTypeConverter;
import
org.apache.gravitino.catalog.jdbc.converter.SqliteColumnDefaultValueConverter;
+import org.apache.gravitino.catalog.jdbc.converter.SqliteExceptionConverter;
import org.apache.gravitino.catalog.jdbc.converter.SqliteTypeConverter;
import org.apache.gravitino.catalog.jdbc.operation.JdbcDatabaseOperations;
import org.apache.gravitino.catalog.jdbc.operation.JdbcTableOperations;
import org.apache.gravitino.catalog.jdbc.operation.SqliteDatabaseOperations;
import org.apache.gravitino.catalog.jdbc.operation.SqliteTableOperations;
+import org.apache.gravitino.connector.CatalogInfo;
+import org.apache.gravitino.exceptions.GravitinoRuntimeException;
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.ValueSource;
public class TestJdbcCatalogPropertiesMetadata {
@@ -61,6 +70,88 @@ public class TestJdbcCatalogPropertiesMetadata {
.shouldValidateOnCreate());
}
+ @ParameterizedTest
+ @CsvSource({
+ "none,none,none,30000",
+ "1250,none,none,1250",
+ "none,2300,none,2300",
+ "1250,2300,none,1250",
+ "none,none,3400,3400",
+ "1250,none,3400,1250"
+ })
+ void testPoolMaxWaitReachesCatalogDataSource(
+ String canonical, String bypass, String dbcpBypass, long expected)
throws Exception {
+ Map<String, String> properties = poolWaitProperties();
+ if (!"none".equals(canonical)) {
+ properties.put(JdbcConfig.POOL_MAX_WAIT_MS.getKey(), canonical);
+ }
+ if (!"none".equals(bypass)) {
+ properties.put("gravitino.bypass." +
JdbcConfig.POOL_MAX_WAIT_MS.getKey(), bypass);
+ }
+ if (!"none".equals(dbcpBypass)) {
+ // The DBCP-native key documented as the bypass example in the JDBC
catalog docs.
+ properties.put("gravitino.bypass.maxWaitMillis", dbcpBypass);
+ }
+ JdbcCatalogPropertiesMetadata metadata = new
JdbcCatalogPropertiesMetadata();
+
Assertions.assertTrue(metadata.containsProperty(JdbcConfig.POOL_MAX_WAIT_MS.getKey()));
+
Assertions.assertFalse(metadata.isHiddenProperty(JdbcConfig.POOL_MAX_WAIT_MS.getKey()));
+
Assertions.assertFalse(metadata.isImmutableProperty(JdbcConfig.POOL_MAX_WAIT_MS.getKey()));
+ try (JdbcCatalogOperations operations = poolWaitOperations()) {
+ operations.initialize(properties, poolWaitCatalogInfo(properties), new
TestJdbcCatalog());
+ BasicDataSource pool = (BasicDataSource)
FieldUtils.readField(operations, "dataSource", true);
+ Assertions.assertEquals(expected, pool.getMaxWaitDuration().toMillis());
+ Assertions.assertEquals("SELECT 1", pool.getValidationQuery());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"0", "-1", "not-a-number"})
+ void testInvalidPoolMaxWaitIsRejected(String value) {
+ Map<String, String> properties = poolWaitProperties();
+ properties.put(JdbcConfig.POOL_MAX_WAIT_MS.getKey(), value);
+ try (JdbcCatalogOperations operations = poolWaitOperations()) {
+ Assertions.assertThrows(
+ GravitinoRuntimeException.class,
+ () ->
+ operations.initialize(
+ properties, poolWaitCatalogInfo(properties), new
TestJdbcCatalog()));
+ }
+ }
+
+ private static Map<String, String> poolWaitProperties() {
+ return new HashMap<>(
+ Map.of(
+ JdbcConfig.JDBC_URL.getKey(),
+ "jdbc:sqlite::memory:",
+ JdbcConfig.JDBC_DRIVER.getKey(),
+ "org.sqlite.JDBC",
+ JdbcConfig.USERNAME.getKey(),
+ "user",
+ JdbcConfig.PASSWORD.getKey(),
+ "password"));
+ }
+
+ private static CatalogInfo poolWaitCatalogInfo(Map<String, String>
properties) {
+ return new CatalogInfo(
+ 1L,
+ "jdbc_catalog",
+ Catalog.Type.RELATIONAL,
+ "jdbc-test",
+ "test",
+ properties,
+
AuditInfo.builder().withCreator("test").withCreateTime(Instant.EPOCH).build(),
+ Namespace.of("metalake"));
+ }
+
+ private static JdbcCatalogOperations poolWaitOperations() {
+ return new JdbcCatalogOperations(
+ new SqliteExceptionConverter(),
+ new SqliteTypeConverter(),
+ new SqliteDatabaseOperations(":memory:"),
+ new SqliteTableOperations(),
+ new SqliteColumnDefaultValueConverter());
+ }
+
private static JdbcCatalog buildCatalog(Map<String, String> extraProperties)
{
Map<String, String> properties =
ImmutableMap.<String, String>builder()
diff --git
a/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/MysqlTablePropertiesMetadata.java
b/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/MysqlTablePropertiesMetadata.java
index e4de0a0cbd..472a10c6f7 100644
---
a/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/MysqlTablePropertiesMetadata.java
+++
b/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/MysqlTablePropertiesMetadata.java
@@ -18,10 +18,12 @@
*/
package org.apache.gravitino.catalog.mysql;
+import static
org.apache.gravitino.catalog.PropertiesMetadataHelpers.checkValueFormat;
import static
org.apache.gravitino.connector.PropertyEntry.enumImmutablePropertyEntry;
import static
org.apache.gravitino.connector.PropertyEntry.integerOptionalPropertyEntry;
import static
org.apache.gravitino.connector.PropertyEntry.stringReservedPropertyEntry;
+import com.google.common.base.Preconditions;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@@ -107,17 +109,23 @@ public class MysqlTablePropertiesMetadata extends
JdbcTablePropertiesMetadata {
@Override
public Map<String, String> transformToJdbcProperties(Map<String, String>
properties) {
- return Collections.unmodifiableMap(
- new HashMap<String, String>() {
- {
- properties.forEach(
- (key, value) -> {
- if (GRAVITINO_CONFIG_TO_MYSQL.containsKey(key)) {
- put(GRAVITINO_CONFIG_TO_MYSQL.get(key), value);
- }
- });
+ Map<String, String> result = new HashMap<>();
+ properties.forEach(
+ (key, value) -> {
+ Preconditions.checkArgument(
+ containsProperty(key),
+ "Unsupported MySQL table property: %s. Supported properties: %s",
+ key,
+ String.join(", ", GRAVITINO_CONFIG_TO_MYSQL.keySet()));
+ if (GRAVITINO_CONFIG_TO_MYSQL.containsKey(key)) {
+ // A null value represents RemoveProperty in
JdbcCatalogOperations. Non-SQL metadata,
+ // such as the internal identifier, must not become options.
+ result.put(
+ GRAVITINO_CONFIG_TO_MYSQL.get(key),
+ value == null ? null :
validateAndEncode(getPropertyEntry(key), value));
}
});
+ return Collections.unmodifiableMap(result);
}
@Override
@@ -135,4 +143,12 @@ public class MysqlTablePropertiesMetadata extends
JdbcTablePropertiesMetadata {
}
});
}
+
+ private static <T> String validateAndEncode(PropertyEntry<T> entry, String
value) {
+ T decoded = checkValueFormat(entry.getName(), value, entry::decode);
+ if (decoded instanceof ENGINE) {
+ return ((ENGINE) decoded).getValue();
+ }
+ return entry.getEncoder().apply(decoded);
+ }
}
diff --git
a/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/operation/MysqlTableOperations.java
b/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/operation/MysqlTableOperations.java
index f50ccca7df..c3e9b7898d 100644
---
a/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/operation/MysqlTableOperations.java
+++
b/catalogs/catalog-jdbc-mysql/src/main/java/org/apache/gravitino/catalog/mysql/operation/MysqlTableOperations.java
@@ -48,6 +48,7 @@ import org.apache.gravitino.StringIdentifier;
import org.apache.gravitino.catalog.jdbc.JdbcColumn;
import org.apache.gravitino.catalog.jdbc.JdbcTable;
import org.apache.gravitino.catalog.jdbc.operation.JdbcTableOperations;
+import org.apache.gravitino.catalog.mysql.MysqlTablePropertiesMetadata;
import org.apache.gravitino.exceptions.NoSuchColumnException;
import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.rel.Column;
@@ -63,6 +64,8 @@ import org.apache.gravitino.rel.types.Types;
/** Table operations for MySQL. */
public class MysqlTableOperations extends JdbcTableOperations {
+ private static final MysqlTablePropertiesMetadata TABLE_PROPERTIES_METADATA =
+ new MysqlTablePropertiesMetadata();
private static final String BACK_QUOTE = "`";
private static final String MYSQL_AUTO_INCREMENT = "AUTO_INCREMENT";
private static final String MYSQL_NOT_SUPPORT_NESTED_COLUMN_MSG =
@@ -132,7 +135,7 @@ public class MysqlTableOperations extends
JdbcTableOperations {
if (MapUtils.isNotEmpty(properties)) {
sqlBuilder.append(
properties.entrySet().stream()
- .map(entry -> String.format("%s = %s", entry.getKey(),
entry.getValue()))
+ .map(entry -> generateTableProperty(entry.getKey(),
entry.getValue()))
.collect(Collectors.joining(",\n", "\n", "")));
}
@@ -503,11 +506,24 @@ public class MysqlTableOperations extends
JdbcTableOperations {
private String generateTableProperties(List<TableChange.SetProperty>
setProperties) {
return setProperties.stream()
.map(
- setProperty ->
- String.format("%s = %s", setProperty.getProperty(),
setProperty.getValue()))
+ setProperty -> generateTableProperty(setProperty.getProperty(),
setProperty.getValue()))
.collect(Collectors.joining(",\n"));
}
+ private String generateTableProperty(String key, String value) {
+ Map<String, String> logicalProperties =
+
TABLE_PROPERTIES_METADATA.convertFromJdbcProperties(Collections.singletonMap(key,
value));
+ Preconditions.checkArgument(
+ !logicalProperties.isEmpty(),
+ "Unsupported MySQL table option: %s. Supported options: %s",
+ key,
+ String.join(", ",
MysqlTablePropertiesMetadata.GRAVITINO_CONFIG_TO_MYSQL.values()));
+ String encoded =
+
TABLE_PROPERTIES_METADATA.transformToJdbcProperties(logicalProperties).get(key);
+ Preconditions.checkArgument(encoded != null, "MySQL table option %s cannot
be null", key);
+ return key + " = " + encoded;
+ }
+
private String updateColumnCommentFieldDefinition(
TableChange.UpdateColumnComment updateColumnComment, JdbcTable
jdbcTable) {
String newComment = updateColumnComment.getNewComment();
diff --git
a/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/TestMysqlTablePropertiesMetadata.java
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/TestMysqlTablePropertiesMetadata.java
new file mode 100644
index 0000000000..228358a3f8
--- /dev/null
+++
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/TestMysqlTablePropertiesMetadata.java
@@ -0,0 +1,214 @@
+/*
+ * 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.mysql;
+
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.Namespace;
+import org.apache.gravitino.StringIdentifier;
+import org.apache.gravitino.catalog.PropertiesMetadataHelpers;
+import org.apache.gravitino.catalog.jdbc.JdbcCatalogOperations;
+import org.apache.gravitino.catalog.jdbc.JdbcColumn;
+import org.apache.gravitino.catalog.jdbc.JdbcTable;
+import org.apache.gravitino.catalog.jdbc.converter.JdbcExceptionConverter;
+import
org.apache.gravitino.catalog.mysql.converter.MysqlColumnDefaultValueConverter;
+import org.apache.gravitino.catalog.mysql.converter.MysqlTypeConverter;
+import org.apache.gravitino.catalog.mysql.operation.MysqlDatabaseOperations;
+import org.apache.gravitino.catalog.mysql.operation.MysqlTableOperations;
+import org.apache.gravitino.connector.CatalogInfo;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.TableChange;
+import org.apache.gravitino.rel.expressions.distributions.Distribution;
+import org.apache.gravitino.rel.expressions.distributions.Distributions;
+import org.apache.gravitino.rel.expressions.sorts.SortOrder;
+import org.apache.gravitino.rel.expressions.transforms.Transform;
+import org.apache.gravitino.rel.expressions.transforms.Transforms;
+import org.apache.gravitino.rel.indexes.Index;
+import org.apache.gravitino.rel.indexes.Indexes;
+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.ValueSource;
+
+class TestMysqlTablePropertiesMetadata {
+ private static final NameIdentifier IDENT =
+ NameIdentifier.of("metalake", "catalog", "database", "table");
+
+ @ParameterizedTest
+ @ValueSource(strings = {"unknown-key", "ENGINE", "ROW_FORMAT", "engine; DROP
TABLE t"})
+ void testCatalogRejectsUnknownPropertiesBeforeCreateOrAlter(String key) {
+ RecordingTableOperations table = new RecordingTableOperations();
+ try (JdbcCatalogOperations operations = operations(table)) {
+ IllegalArgumentException create =
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> create(operations,
Map.of(key, "MEMORY")));
+ Assertions.assertTrue(create.getMessage().contains(key));
+ Assertions.assertTrue(
+ create.getMessage().contains("Supported properties:
auto-increment-offset, engine"),
+ create.getMessage());
+ IllegalArgumentException alter =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> operations.alterTable(IDENT, TableChange.setProperty(key,
"MEMORY")));
+ Assertions.assertTrue(alter.getMessage().contains(key));
+ IllegalArgumentException remove =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> operations.alterTable(IDENT,
TableChange.removeProperty(key)));
+ Assertions.assertTrue(remove.getMessage().contains(key));
+ Assertions.assertEquals(0, table.writes);
+ }
+ }
+
+ @ParameterizedTest
+ @CsvSource({"engine,InnoDB; DROP TABLE t", "auto-increment-offset,42; DROP
TABLE t"})
+ void testInvalidDeclaredValuesAreRejectedBeforeCreate(String key, String
value) {
+ RecordingTableOperations table = new RecordingTableOperations();
+ try (JdbcCatalogOperations operations = operations(table)) {
+ IllegalArgumentException error =
+ Assertions.assertThrows(
+ IllegalArgumentException.class, () -> create(operations,
Map.of(key, value)));
+ Assertions.assertTrue(error.getMessage().contains(key));
+ Assertions.assertEquals(0, table.writes);
+ }
+ }
+
+ @Test
+ void testValidPropertiesRoundTripAndMetadataDoesNotBecomeSql() {
+ RecordingTableOperations table = new RecordingTableOperations();
+ try (JdbcCatalogOperations operations = operations(table)) {
+ Table created =
+ create(operations, Map.of("engine", "InNoDB",
"auto-increment-offset", "+0042"));
+ Assertions.assertEquals(Map.of("ENGINE", "InnoDB", "AUTO_INCREMENT",
"42"), table.properties);
+ Assertions.assertEquals(
+ Map.of("engine", "InnoDB", "auto-increment-offset", "42"),
created.properties());
+ Table loaded = operations.loadTable(IDENT);
+ Assertions.assertEquals("InnoDB", loaded.properties().get("engine"));
+ Assertions.assertEquals("42",
loaded.properties().get("auto-increment-offset"));
+ Assertions.assertEquals(1, table.writes);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"engine", "auto-increment-offset"})
+ void testDeclaredPropertiesRemainImmutable(String key) {
+ IllegalArgumentException error =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ PropertiesMetadataHelpers.validatePropertyForAlter(
+ new MysqlTablePropertiesMetadata(), Map.of(key, "42"),
Map.of()));
+ Assertions.assertTrue(error.getMessage().contains("immutable"));
+ Map<String, String> removal = new HashMap<>();
+ removal.put(key, null);
+ Assertions.assertTrue(
+ new
MysqlTablePropertiesMetadata().transformToJdbcProperties(removal).containsValue(null));
+ }
+
+ private static Table create(JdbcCatalogOperations operations, Map<String,
String> properties) {
+ return operations.createTable(
+ IDENT,
+ new Column[0],
+ "comment",
+ StringIdentifier.newPropertiesWithId(StringIdentifier.fromId(1L),
properties),
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ new SortOrder[0],
+ Indexes.EMPTY_INDEXES);
+ }
+
+ private static JdbcCatalogOperations operations(RecordingTableOperations
table) {
+ JdbcCatalogOperations operations =
+ new JdbcCatalogOperations(
+ new JdbcExceptionConverter(),
+ new MysqlTypeConverter(),
+ new MysqlDatabaseOperations(),
+ table,
+ new MysqlColumnDefaultValueConverter());
+ Map<String, String> conf =
+ Map.of(
+ "jdbc-url",
+ "jdbc:mysql://127.0.0.1:1/unused",
+ "jdbc-driver",
+ "com.mysql.cj.jdbc.Driver",
+ "jdbc-user",
+ "test",
+ "jdbc-password",
+ "test");
+ CatalogInfo info =
+ new CatalogInfo(
+ 1L,
+ "catalog",
+ Catalog.Type.RELATIONAL,
+ "jdbc-mysql",
+ "test",
+ conf,
+
AuditInfo.builder().withCreator("test").withCreateTime(Instant.EPOCH).build(),
+ Namespace.of("metalake"));
+ operations.initialize(conf, info, new MysqlCatalog());
+ return operations;
+ }
+
+ private static class RecordingTableOperations extends MysqlTableOperations {
+ private int writes;
+ private Map<String, String> properties = Collections.emptyMap();
+ private String comment;
+
+ /** Records a create after catalog property validation and translation. */
+ @Override
+ public void create(
+ String databaseName,
+ String tableName,
+ JdbcColumn[] columns,
+ String comment,
+ Map<String, String> properties,
+ Transform[] partitioning,
+ Distribution distribution,
+ Index[] indexes,
+ SortOrder[] sortOrders) {
+ writes++;
+ this.properties = new HashMap<>(properties);
+ this.comment = comment;
+ }
+
+ /** Records alterations reaching the table operation boundary. */
+ @Override
+ public void alterTable(String databaseName, String tableName,
TableChange... changes) {
+ writes++;
+ }
+
+ /** Returns the state recorded by create. */
+ @Override
+ public JdbcTable load(String databaseName, String tableName) {
+ return JdbcTable.builder()
+ .withName(tableName)
+ .withColumns(new JdbcColumn[0])
+ .withProperties(properties)
+ .withComment(comment)
+ .build();
+ }
+ }
+}
diff --git
a/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/operation/TestMysqlTableOperations.java
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/operation/TestMysqlTableOperations.java
index b48e8a9e08..f9b38d6419 100644
---
a/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/operation/TestMysqlTableOperations.java
+++
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/operation/TestMysqlTableOperations.java
@@ -37,7 +37,6 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.catalog.jdbc.JdbcColumn;
import org.apache.gravitino.catalog.jdbc.JdbcTable;
import org.apache.gravitino.catalog.jdbc.utils.DataSourceUtils;
-import org.apache.gravitino.exceptions.GravitinoRuntimeException;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.TableChange;
import org.apache.gravitino.rel.expressions.distributions.Distributions;
@@ -155,15 +154,13 @@ public class TestMysqlTableOperations extends TestMysql {
assertionsTableInfo(
newName, tableComment, alterColumns, properties, indexes,
Transforms.EMPTY_TRANSFORM, load);
- // Detect unsupported properties
+ // Reject an unsupported engine before generating or executing DDL.
TableChange setProperty = TableChange.setProperty(MYSQL_ENGINE_KEY, "ABC");
- GravitinoRuntimeException gravitinoRuntimeException =
+ IllegalArgumentException invalidEngine =
Assertions.assertThrows(
- GravitinoRuntimeException.class,
+ IllegalArgumentException.class,
() -> TABLE_OPERATIONS.alterTable(TEST_DB_NAME.toString(),
newName, setProperty));
- Assertions.assertTrue(
- StringUtils.contains(
- gravitinoRuntimeException.getMessage(), "Unknown storage engine
'ABC'"));
+ Assertions.assertTrue(invalidEngine.getMessage().contains("engine"));
// delete column
TABLE_OPERATIONS.alterTable(
diff --git
a/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/operation/TestMysqlTableOperationsSqlGeneration.java
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/operation/TestMysqlTableOperationsSqlGeneration.java
index ad8966c2c8..0b9f51a1a0 100644
---
a/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/operation/TestMysqlTableOperationsSqlGeneration.java
+++
b/catalogs/catalog-jdbc-mysql/src/test/java/org/apache/gravitino/catalog/mysql/operation/TestMysqlTableOperationsSqlGeneration.java
@@ -19,10 +19,13 @@
package org.apache.gravitino.catalog.mysql.operation;
import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Stream;
import org.apache.gravitino.catalog.jdbc.JdbcColumn;
import org.apache.gravitino.catalog.jdbc.JdbcTable;
import
org.apache.gravitino.catalog.jdbc.converter.JdbcColumnDefaultValueConverter;
import org.apache.gravitino.catalog.jdbc.converter.JdbcExceptionConverter;
+import org.apache.gravitino.catalog.jdbc.utils.ConnectionCountingDataSource;
import org.apache.gravitino.catalog.mysql.converter.MysqlTypeConverter;
import org.apache.gravitino.rel.TableChange;
import org.apache.gravitino.rel.expressions.distributions.Distributions;
@@ -32,6 +35,9 @@ import org.apache.gravitino.rel.indexes.Indexes;
import org.apache.gravitino.rel.types.Types;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
public class TestMysqlTableOperationsSqlGeneration {
@@ -42,6 +48,11 @@ public class TestMysqlTableOperationsSqlGeneration {
super.columnDefaultValueConverter = new
JdbcColumnDefaultValueConverter();
}
+ private TestableMysqlTableOperations(ConnectionCountingDataSource
connections) {
+ this();
+ dataSource = connections.dataSource();
+ }
+
public String createTableSql(String tableName, JdbcColumn[] columns) {
return createTableSql(tableName, columns, "comment");
}
@@ -68,6 +79,77 @@ public class TestMysqlTableOperationsSqlGeneration {
}
}
+ @ParameterizedTest
+ @MethodSource("invalidTableOptions")
+ void testInvalidOptionsAreRejectedBeforeCreateAndAlterSql(String key, String
value) {
+ ConnectionCountingDataSource connections = new
ConnectionCountingDataSource();
+ TestableMysqlTableOperations ops = new
TestableMysqlTableOperations(connections);
+ JdbcColumn column =
+
JdbcColumn.builder().withName("col1").withType(Types.IntegerType.get()).build();
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ops.create(
+ "database",
+ "test_table",
+ new JdbcColumn[] {column},
+ "comment",
+ Collections.singletonMap(key, value),
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ Indexes.EMPTY_INDEXES));
+ Assertions.assertEquals(0, connections.totalBorrows());
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () -> ops.alterTable("database", "test_table",
TableChange.setProperty(key, value)));
+ Assertions.assertTrue(connections.executedSql().isEmpty());
+ Assertions.assertEquals(0, connections.borrowed());
+ // branch-1.3 generates ALTER SQL inside the execution connection;
validation still prevents
+ // DDL.
+ Assertions.assertEquals(1, connections.peakBorrowed());
+ Assertions.assertEquals(1, connections.totalBorrows());
+ }
+
+ @Test
+ void testUnsupportedSqlOptionNamesSupportedOptions() {
+ TestableMysqlTableOperations ops = new TestableMysqlTableOperations();
+ JdbcColumn column =
+
JdbcColumn.builder().withName("col1").withType(Types.IntegerType.get()).build();
+ IllegalArgumentException error =
+ Assertions.assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ ops.generateCreateTableSql(
+ "test_table",
+ new JdbcColumn[] {column},
+ null,
+ Map.of("ROW_FORMAT", "DYNAMIC"),
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ Indexes.EMPTY_INDEXES));
+ Assertions.assertTrue(
+ error.getMessage().contains("Supported options: AUTO_INCREMENT,
ENGINE"),
+ error.getMessage());
+ }
+
+ @Test
+ void testSupportedSqlOptionsAreNormalized() {
+ TestableMysqlTableOperations ops = new TestableMysqlTableOperations();
+ JdbcColumn column =
+
JdbcColumn.builder().withName("col1").withType(Types.IntegerType.get()).build();
+ String sql =
+ ops.generateCreateTableSql(
+ "test_table",
+ new JdbcColumn[] {column},
+ null,
+ Map.of("ENGINE", "InNoDB", "AUTO_INCREMENT", "+0042"),
+ Transforms.EMPTY_TRANSFORM,
+ Distributions.NONE,
+ Indexes.EMPTY_INDEXES);
+ Assertions.assertTrue(sql.contains("ENGINE = InnoDB"), sql);
+ Assertions.assertTrue(sql.contains("AUTO_INCREMENT = 42"), sql);
+ }
+
@Test
public void testCreateTableWithEmptyStringDefaultValue() {
TestableMysqlTableOperations ops = new TestableMysqlTableOperations();
@@ -147,4 +229,14 @@ public class TestMysqlTableOperationsSqlGeneration {
Assertions.assertTrue(alterSql.contains("ADD COLUMN `col2`"), alterSql);
Assertions.assertTrue(alterSql.contains("COMMENT 'owner\\\\''s
\"comment\"; --'"), alterSql);
}
+
+ private static Stream<Arguments> invalidTableOptions() {
+ return Stream.of(
+ Arguments.of("ENGINE", "ABC"),
+ Arguments.of("ENGINE", "InnoDB, ROW_FORMAT=DYNAMIC"),
+ Arguments.of("AUTO_INCREMENT", "1; DROP TABLE t"),
+ Arguments.of("ROW_FORMAT", "DYNAMIC"),
+ Arguments.of("ENGINE; DROP TABLE t", "MEMORY"),
+ Arguments.of("AUTO_INCREMENT", null));
+ }
}
diff --git a/docs/jdbc-clickhouse-catalog.md b/docs/jdbc-clickhouse-catalog.md
index c0f7ee27e4..fdc7be7a56 100644
--- a/docs/jdbc-clickhouse-catalog.md
+++ b/docs/jdbc-clickhouse-catalog.md
@@ -56,7 +56,7 @@ When using the JDBC catalog you must provide `jdbc-url`,
`jdbc-driver`, `jdbc-us
| `jdbc-password` | JDBC password
| (none) | Yes |
| `jdbc.pool.min-size` | Minimum pool size
| `2` | No |
| `jdbc.pool.max-size` | Maximum pool size
| `10` | No |
-| `jdbc.pool.max-wait-ms` | Max wait time for a connection
| `30000` | No |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned; takes precedence over
`gravitino.bypass.maxWaitMillis`. | `30000` | No |
### Create a ClickHouse Catalog
diff --git a/docs/jdbc-doris-catalog.md b/docs/jdbc-doris-catalog.md
index 47bf6e02aa..49e576aceb 100644
--- a/docs/jdbc-doris-catalog.md
+++ b/docs/jdbc-doris-catalog.md
@@ -50,7 +50,7 @@ Besides the [common catalog
properties](./gravitino-server-config.md#catalog-pro
| `jdbc.pool.min-size` | The minimum number of connections in the pool. `2`
by default.
| `2` | No |
| `jdbc.pool.max-size` | The maximum number of connections in the pool.
`10` by default.
| `10` | No |
| `replication_num` | The number of replications for the table. If not
specified and the number of backend servers less than 3, then the default value
is 1; If not specified and the number of backend servers greater or equals to
3, the default value (3) in Doris server will be used. For more, see the
[doc](https://doris.apache.org/docs/1.2/sql-manual/sql-reference/Data-Definition-Statements/Create/CREATE-TABLE/)
| `1` or `3` | No |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned. `30000` by default.
| `30000` | No |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned; takes precedence over
`gravitino.bypass.maxWaitMillis`. | `30000` | No |
Before using the Doris Catalog, you must download the corresponding JDBC
driver to the `catalogs/jdbc-doris/libs` directory.
Gravitino doesn't package the JDBC driver for Doris due to licensing issues.
diff --git a/docs/jdbc-mysql-catalog.md b/docs/jdbc-mysql-catalog.md
index bd2964861b..534baa1bad 100644
--- a/docs/jdbc-mysql-catalog.md
+++ b/docs/jdbc-mysql-catalog.md
@@ -49,7 +49,7 @@ Besides the [common catalog
properties](./gravitino-server-config.md#catalog-pro
| `jdbc-password` | The JDBC password.
| (none) | Yes |
| `jdbc.pool.min-size` | The minimum number of connections in the pool. `2`
by default. | `2` | No |
| `jdbc.pool.max-size` | The maximum number of connections in the pool.
`10` by default. | `10` | No
|
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned. `30000` by default. | `30000` | No |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned; takes precedence over
`gravitino.bypass.maxWaitMillis`. | `30000` | No |
:::caution
Gravitino does not package the MySQL JDBC driver, because MySQL Connector/J is
@@ -222,7 +222,9 @@ Although MySQL itself does not support table properties,
Gravitino offers table
:::
:::caution
-- Doesn't support remove table properties. You can only add or modify
properties, not delete properties.
+For MySQL table options, use the Gravitino property names listed below.
Unknown properties, including SQL option names such as `ENGINE` or
`ROW_FORMAT`, are rejected. Use the Gravitino property name `engine` instead.
+
+These properties can only be specified when creating a table; they cannot be
changed or removed afterwards. Values are validated against their declared
types before DDL is executed.
:::
| Property Name | Description
| Default Value | Required | Reserved | Immutable |
diff --git a/docs/jdbc-oceanbase-catalog.md b/docs/jdbc-oceanbase-catalog.md
index 9e0b99e7a5..40c99f58a8 100644
--- a/docs/jdbc-oceanbase-catalog.md
+++ b/docs/jdbc-oceanbase-catalog.md
@@ -47,7 +47,7 @@ Besides the [common catalog
properties](./gravitino-server-config.md#catalog-pro
| `jdbc-password` | The JDBC password.
| (none) | Yes |
| `jdbc.pool.min-size` | The minimum number of connections in the pool. `2`
by default.
| `2` | No |
| `jdbc.pool.max-size` | The maximum number of connections in the pool.
`10` by default.
| `10` | No |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned. `30000` by default.
| `30000` | No |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned; takes precedence over
`gravitino.bypass.maxWaitMillis`. | `30000` | No |
:::caution
Gravitino does not package the OceanBase JDBC driver due to licensing, so you
diff --git a/docs/jdbc-postgresql-catalog.md b/docs/jdbc-postgresql-catalog.md
index 70ab6b04bc..b5def139a8 100644
--- a/docs/jdbc-postgresql-catalog.md
+++ b/docs/jdbc-postgresql-catalog.md
@@ -48,7 +48,7 @@ Besides the [common catalog
properties](./gravitino-server-config.md#catalog-pro
| `jdbc-password` | The JDBC password.
| (none) | Yes |
| `jdbc.pool.min-size` | The minimum number of connections in the pool. `2`
by default.
| `2` | No |
| `jdbc.pool.max-size` | The maximum number of connections in the pool.
`10` by default.
| `10` | No |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned. `30000` by default.
| `30000` | No |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned; takes precedence over
`gravitino.bypass.maxWaitMillis`. | `30000` | No |
:::caution
Download the corresponding JDBC driver to the `catalogs/jdbc-postgresql/libs`
directory.
diff --git a/docs/jdbc-starrocks-catalog.md b/docs/jdbc-starrocks-catalog.md
index a03d268cbe..ad2d9850ce 100644
--- a/docs/jdbc-starrocks-catalog.md
+++ b/docs/jdbc-starrocks-catalog.md
@@ -48,7 +48,7 @@ Besides the [common catalog
properties](./gravitino-server-config.md#catalog-pro
| `jdbc-password` | The JDBC password.
| (none) | Yes |
| `jdbc.pool.min-size` | The minimum number of connections in the pool. `2`
by default. | `2` | No |
| `jdbc.pool.max-size` | The maximum number of connections in the pool.
`10` by default. | `10` | No |
-| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned. `30000` by default. | `30000` | No |
+| `jdbc.pool.max-wait-ms` | The maximum Duration that the pool will wait for a
connection to be returned; takes precedence over
`gravitino.bypass.maxWaitMillis`. | `30000` | No |
Before using the StarRocks Catalog, you must download the corresponding JDBC
driver to the `catalogs/jdbc-starrocks/libs` directory.
Gravitino doesn't package the JDBC driver for StarRocks due to licensing
issues.
diff --git
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/jdbc/JdbcPropertiesConverter.java
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/jdbc/JdbcPropertiesConverter.java
index 611952e02d..d0994d9259 100644
---
a/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/jdbc/JdbcPropertiesConverter.java
+++
b/flink-connector/flink-common/src/main/java/org/apache/gravitino/flink/connector/jdbc/JdbcPropertiesConverter.java
@@ -19,8 +19,10 @@
package org.apache.gravitino.flink.connector.jdbc;
+import com.google.common.collect.ImmutableSet;
import java.util.HashMap;
import java.util.Map;
+import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
@@ -35,6 +37,13 @@ public abstract class JdbcPropertiesConverter
private static final Pattern jdbcUrlPattern =
Pattern.compile("(jdbc:\\w+://[^:/]+(?::\\d+)?)");
+ private static final Set<String> INJECTED_TABLE_OPTIONS =
+ ImmutableSet.of(
+ JdbcPropertiesConstants.FLINK_JDBC_TABLE_DATABASE_URL,
+ JdbcPropertiesConstants.FLINK_JDBC_TABLE_NAME,
+ JdbcPropertiesConstants.FLINK_JDBC_USER,
+ JdbcPropertiesConstants.FLINK_JDBC_PASSWORD);
+
@Override
public Map<String, String> toGravitinoCatalogProperties(Configuration
flinkConf) {
Map<String, String> gravitinoCatalogProperties =
@@ -86,6 +95,18 @@ public abstract class JdbcPropertiesConverter
return JdbcPropertiesConstants.gravitinoToFlinkMap.get(configKey);
}
+ /**
+ * Drops the connection options that {@link #toFlinkTableProperties} injects
for scans. Flink
+ * copies them into the options of {@code CREATE TABLE ... LIKE}, but they
describe the catalog
+ * connection rather than the table, and must not reach the server as table
properties.
+ */
+ @Override
+ public Map<String, String> toGravitinoTableProperties(Map<String, String>
flinkProperties) {
+ Map<String, String> gravitinoProperties = new HashMap<>(flinkProperties);
+ gravitinoProperties.keySet().removeAll(INJECTED_TABLE_OPTIONS);
+ return gravitinoProperties;
+ }
+
@Override
public Map<String, String> toFlinkTableProperties(
Map<String, String> flinkCatalogProperties,
diff --git
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/FlinkCommonIT.java
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/FlinkCommonIT.java
index 159fe00775..222a5a370c 100644
---
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/FlinkCommonIT.java
+++
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/FlinkCommonIT.java
@@ -95,6 +95,15 @@ public abstract class FlinkCommonIT extends FlinkEnvIT {
return true;
}
+ /**
+ * Whether the catalog accepts arbitrary table properties, such as {@code
'test key' = 'test
+ * value'}, at creation time. Catalogs that validate table properties
against a fixed set reject
+ * them.
+ */
+ protected boolean supportsCustomTableProperties() {
+ return true;
+ }
+
protected boolean defaultValueWithNullLiterals() {
return false;
}
@@ -296,16 +305,15 @@ public abstract class FlinkCommonIT extends FlinkEnvIT {
"CREATE TABLE %s "
+ "(string_type STRING COMMENT 'string_type', "
+ " double_type DOUBLE COMMENT 'double_type')"
- + " COMMENT '%s' WITH ("
- + "'%s' = '%s')",
- tableName, comment, key, value);
+ + " COMMENT '%s'%s",
+ tableName, comment, customTablePropertiesClause(key, value));
TestUtils.assertTableResult(result, ResultKind.SUCCESS);
Table table =
catalog.asTableCatalog().loadTable(NameIdentifier.of(databaseName, tableName));
Assertions.assertNotNull(table);
Assertions.assertEquals(comment, table.comment());
- if (supportTablePropertiesOperation()) {
+ if (supportTablePropertiesOperation() &&
supportsCustomTableProperties()) {
Assertions.assertEquals(value, table.properties().get(key));
}
Column[] columns =
@@ -350,9 +358,8 @@ public abstract class FlinkCommonIT extends FlinkEnvIT {
+ " cc int,"
+ " PRIMARY KEY (aa,bb) NOT ENFORCED"
+ ")"
- + " COMMENT '%s' WITH ("
- + "'%s' = '%s')",
- tableName, comment, key, value);
+ + " COMMENT '%s'%s",
+ tableName, comment, customTablePropertiesClause(key, value));
Table table =
catalog.asTableCatalog().loadTable(NameIdentifier.of(databaseName, tableName));
Assertions.assertEquals(1, table.index().length);
@@ -492,7 +499,9 @@ public abstract class FlinkCommonIT extends FlinkEnvIT {
NameIdentifier.of(databaseName, "test_desc_table"),
columns,
comment,
- ImmutableMap.of("k1", "v1"));
+ supportsCustomTableProperties()
+ ? ImmutableMap.of("k1", "v1")
+ : ImmutableMap.of());
Optional<org.apache.flink.table.catalog.Catalog> flinkCatalog =
tableEnv.getCatalog(catalog.name());
@@ -708,10 +717,8 @@ public abstract class FlinkCommonIT extends FlinkEnvIT {
"CREATE TABLE %s "
+ "(user_id BIGINT COMMENT 'USER_ID', "
+ " order_amount INT COMMENT 'ORDER_AMOUNT')"
- + " COMMENT 'test comment'"
- + " WITH ("
- + "'%s' = '%s')",
- tableName, "test key", "test value");
+ + " COMMENT 'test comment'%s",
+ tableName, customTablePropertiesClause("test key", "test
value"));
TestUtils.assertTableResult(result, ResultKind.SUCCESS);
result = sql("ALTER TABLE %s MODIFY order_amount INT COMMENT 'new
comment2'", tableName);
TestUtils.assertTableResult(result, ResultKind.SUCCESS);
@@ -1111,4 +1118,8 @@ public abstract class FlinkCommonIT extends FlinkEnvIT {
true,
supportDropCascade());
}
+
+ private String customTablePropertiesClause(String key, String value) {
+ return supportsCustomTableProperties() ? String.format(" WITH ('%s' =
'%s')", key, value) : "";
+ }
}
diff --git
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/jdbc/FlinkJdbcMysqlCatalogIT.java
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/jdbc/FlinkJdbcMysqlCatalogIT.java
index 23989bc4cd..1ca152cfc3 100644
---
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/jdbc/FlinkJdbcMysqlCatalogIT.java
+++
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/integration/test/jdbc/FlinkJdbcMysqlCatalogIT.java
@@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableMap;
import java.util.Map;
import org.apache.flink.table.api.ValidationException;
import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.credential.CredentialConstants;
import org.apache.gravitino.credential.JdbcCredential;
import org.apache.gravitino.flink.connector.integration.test.FlinkCommonIT;
@@ -57,6 +58,11 @@ public abstract class FlinkJdbcMysqlCatalogIT extends
FlinkCommonIT {
return false;
}
+ @Override
+ protected boolean supportsCustomTableProperties() {
+ return false;
+ }
+
@Override
protected String defaultDatabaseName() {
return MYSQL_CATALOG_MYSQL_IT.name();
@@ -205,4 +211,30 @@ public abstract class FlinkJdbcMysqlCatalogIT extends
FlinkCommonIT {
catalogName, mysqlUrl, mysqlUsername,
mysqlDefaultDatabase)),
"Missing required options are:\n" + "\n" + "password");
}
+
+ @Test
+ public void testCreateTableLikeDoesNotForwardConnectorOptions() {
+ String databaseName = "test_create_table_like_db";
+ doWithSchema(
+ currentCatalog(),
+ databaseName,
+ catalog -> {
+ sql("CREATE TABLE source_table (id INT COMMENT 'id', name STRING
COMMENT 'name')");
+ // LIKE copies the options that getTable() exposes for the source
table, which include the
+ // JDBC connection options the connector injects for scans.
+ sql("CREATE TABLE like_table LIKE source_table");
+
+ Map<String, String> properties =
+ catalog
+ .asTableCatalog()
+ .loadTable(NameIdentifier.of(databaseName, "like_table"))
+ .properties();
+ Assertions.assertFalse(
+
properties.containsKey(JdbcPropertiesConstants.FLINK_JDBC_PASSWORD));
+ Assertions.assertFalse(
+
properties.containsKey(JdbcPropertiesConstants.FLINK_JDBC_TABLE_DATABASE_URL));
+ },
+ true,
+ supportDropCascade());
+ }
}
diff --git
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/jdbc/AbstractJdbcPropertiesConverterTestSuite.java
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/jdbc/AbstractJdbcPropertiesConverterTestSuite.java
index d89fbf5401..c40eb6277e 100644
---
a/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/jdbc/AbstractJdbcPropertiesConverterTestSuite.java
+++
b/flink-connector/flink-common/src/test/java/org/apache/gravitino/flink/connector/jdbc/AbstractJdbcPropertiesConverterTestSuite.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
import java.util.Map;
import org.apache.flink.configuration.Configuration;
+import org.apache.flink.table.catalog.ObjectPath;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -168,4 +169,24 @@ public abstract class
AbstractJdbcPropertiesConverterTestSuite {
Assertions.assertTrue(ex.getMessage().contains(JdbcPropertiesConstants.GRAVITINO_JDBC_URL));
}
+
+ @Test
+ public void testInjectedTableOptionsDoNotBecomeGravitinoTableProperties() {
+ JdbcPropertiesConverter converter = getConverter(catalogProperties);
+ Map<String, String> flinkTableOptions =
+ new HashMap<>(
+ converter.toFlinkTableProperties(
+ converter.toFlinkCatalogProperties(catalogProperties),
+ ImmutableMap.of(),
+ new ObjectPath("db", "source_table")));
+ // CREATE TABLE ... LIKE copies the scan options alongside the user's own
options.
+ flinkTableOptions.put("engine", "InnoDB");
+
+ Map<String, String> gravitinoProperties =
+ converter.toGravitinoTableProperties(flinkTableOptions);
+
+ Assertions.assertEquals(ImmutableMap.of("engine", "InnoDB"),
gravitinoProperties);
+ Assertions.assertEquals(
+ password,
flinkTableOptions.get(JdbcPropertiesConstants.FLINK_JDBC_PASSWORD));
+ }
}
diff --git
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/JdbcPropertiesConverter.java
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/JdbcPropertiesConverter.java
index 7bd2a86b85..7c80232d5e 100644
---
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/JdbcPropertiesConverter.java
+++
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/JdbcPropertiesConverter.java
@@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
import java.util.Map;
import org.apache.gravitino.spark.connector.PropertiesConverter;
+import org.apache.spark.sql.connector.catalog.TableCatalog;
public class JdbcPropertiesConverter implements PropertiesConverter {
@@ -84,7 +85,10 @@ public class JdbcPropertiesConverter implements
PropertiesConverter {
}
return new HashMap<>();
} else {
- return new HashMap<>(properties);
+ Map<String, String> tableProperties = new HashMap<>(properties);
+ // Spark adds ownership to CREATE TABLE properties; JDBC ownership is
not a table option.
+
tableProperties.keySet().removeIf(TableCatalog.PROP_OWNER::equalsIgnoreCase);
+ return tableProperties;
}
}
diff --git
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/jdbc/TestJdbcPropertiesConverter.java
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/jdbc/TestJdbcPropertiesConverter.java
index 5d3e4d065f..1703e355d0 100644
---
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/jdbc/TestJdbcPropertiesConverter.java
+++
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/jdbc/TestJdbcPropertiesConverter.java
@@ -23,11 +23,53 @@ import com.google.common.collect.ImmutableMap;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
public class TestJdbcPropertiesConverter {
private final JdbcPropertiesConverter jdbcPropertiesConverter =
JdbcPropertiesConverter.getInstance();
+ @Test
+ void testGetPGInstance() {
+ JdbcPropertiesConverter instance = JdbcPropertiesConverter.getPGInstance();
+ Assertions.assertNotNull(instance);
+ Assertions.assertSame(instance, JdbcPropertiesConverter.getPGInstance());
+ Assertions.assertNotSame(instance, JdbcPropertiesConverter.getInstance());
+
+ Map<String, String> emptyProps =
instance.toGravitinoTableProperties(ImmutableMap.of());
+ Assertions.assertTrue(emptyProps.isEmpty());
+
+ Map<String, String> ownerProps =
+ instance.toGravitinoTableProperties(ImmutableMap.of("owner", "alice"));
+ Assertions.assertTrue(ownerProps.isEmpty());
+
+ Map<String, String> upperOwnerProps =
+ instance.toGravitinoTableProperties(ImmutableMap.of("OWNER", "bob"));
+ Assertions.assertTrue(upperOwnerProps.isEmpty());
+
+ Assertions.assertThrows(
+ UnsupportedOperationException.class,
+ () -> instance.toGravitinoTableProperties(ImmutableMap.of("someKey",
"value")));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"owner", "OWNER"})
+ void testSparkOwnershipIsNotForwardedAsATableOption(String ownerKey) {
+ Map<String, String> properties =
+ ImmutableMap.of(ownerKey, "alice", "engine", "InnoDB", "unknown-key",
"value");
+
+ Map<String, String> converted =
jdbcPropertiesConverter.toGravitinoTableProperties(properties);
+
+ Assertions.assertEquals(ImmutableMap.of("engine", "InnoDB", "unknown-key",
"value"), converted);
+ // Keep caller-owned input intact and forward unknown user keys for
server-side validation.
+ Assertions.assertEquals("alice", properties.get(ownerKey));
+ Assertions.assertTrue(
+ jdbcPropertiesConverter
+ .toGravitinoTableProperties(ImmutableMap.of(ownerKey, "alice"))
+ .isEmpty());
+ }
+
@Test
void testCatalogProperties() {
String url = "jdbc-url";