This is an automated email from the ASF dual-hosted git repository.
davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git
The following commit(s) were added to refs/heads/dev by this push:
new e9c446638c [Fix][API] Fix backward compatibility issue in
CatalogFactory optionRule validation (#11165)
e9c446638c is described below
commit e9c446638c55fa16fcc0bc03de8eb0eff0ae26c8
Author: zhiwei.niu <[email protected]>
AuthorDate: Thu Jun 25 20:44:26 2026 +0800
[Fix][API] Fix backward compatibility issue in CatalogFactory optionRule
validation (#11165)
Co-authored-by: Daniel <[email protected]>
---
.../introduction/concepts/incompatible-changes.md | 7 ++
.../introduction/concepts/incompatible-changes.md | 7 ++
.../SqlServerIncrementalSourceFactoryTest.java | 33 ++++++
.../jdbc/catalog/dm/DamengCatalogFactory.java | 2 +-
.../jdbc/catalog/duckdb/DuckDBCatalogFactory.java | 16 ++-
.../jdbc/catalog/highgo/HighGoCatalogFactory.java | 2 +-
.../jdbc/catalog/iris/IrisCatalogFactory.java | 2 +-
.../catalog/kingbase/KingbaseCatalogFactory.java | 2 +-
.../jdbc/catalog/mysql/MySqlCatalogFactory.java | 2 +-
.../catalog/opengauss/OpenGaussCatalogFactory.java | 2 +-
.../jdbc/catalog/oracle/OracleCatalogFactory.java | 31 +++++-
.../jdbc/catalog/psql/PostgresCatalogFactory.java | 2 +-
.../catalog/redshift/RedshiftCatalogFactory.java | 2 +-
.../catalog/saphana/SapHanaCatalogFactory.java | 30 ++++-
.../jdbc/catalog/saphana/SapHanaURLParser.java | 3 +-
.../catalog/sqlserver/SqlServerCatalogFactory.java | 31 +++++-
.../jdbc/catalog/tidb/TiDBCatalogFactory.java | 2 +-
.../jdbc/catalog/xugu/XuguCatalogFactory.java | 5 +-
.../seatunnel/jdbc/config/JdbcCommonOptions.java | 50 +++++----
.../jdbc/catalog/JdbcCatalogFactoryTest.java | 122 +++++++++++++++++++--
.../lance/catalog/LanceCatalogFactory.java | 4 +
21 files changed, 301 insertions(+), 56 deletions(-)
diff --git a/docs/en/introduction/concepts/incompatible-changes.md
b/docs/en/introduction/concepts/incompatible-changes.md
index e5db071e74..6ea2dc2ac1 100644
--- a/docs/en/introduction/concepts/incompatible-changes.md
+++ b/docs/en/introduction/concepts/incompatible-changes.md
@@ -61,6 +61,13 @@ You need to check this document before you upgrade to
related version.
### Configuration Changes
+- **Breaking Change: CatalogFactory creation path now validates
`optionRule()`**
+ - **Affected component**: `seatunnel-api` —
`FactoryUtil.createOptionalCatalog()`
+ - **Description**: The `FactoryUtil.createOptionalCatalog()` method now
calls `ConfigValidator.validate(catalogFactory.optionRule())` before creating a
catalog instance. Previously, no validation was performed on the catalog
factory's option rules during catalog creation.
+ - **Impact**: Catalog factories whose `optionRule()` declares options as
`required` that are not always present in the config passed to
`createOptionalCatalog()` will now throw `OptionValidationException`. This
primarily affects the JDBC connector path via `JdbcCatalogUtils.findCatalog()`.
+ - **Migration Guide**: If you have a custom `CatalogFactory` implementation,
ensure that its `optionRule()` accurately reflects which options are truly
mandatory vs optional in the config that reaches it at runtime.
+
+
### Connector Changes
- **Breaking Change: Iceberg Connector — source table primary key is no longer
silently inherited**
diff --git a/docs/zh/introduction/concepts/incompatible-changes.md
b/docs/zh/introduction/concepts/incompatible-changes.md
index 97907ef653..8a6d7b0a51 100644
--- a/docs/zh/introduction/concepts/incompatible-changes.md
+++ b/docs/zh/introduction/concepts/incompatible-changes.md
@@ -60,6 +60,13 @@
### 配置变更
+- **破坏性变更:CatalogFactory 创建路径现在会校验 `optionRule()`**
+ - **影响范围**:`seatunnel-api` — `FactoryUtil.createOptionalCatalog()`
+ - **变更说明**:`FactoryUtil.createOptionalCatalog()` 方法现在在创建 catalog 实例之前会调用
`ConfigValidator.validate(catalogFactory.optionRule())` 进行校验。此前,catalog 创建路径不会对
catalog factory 的 option rules 执行任何校验。
+ - **影响**:如果 catalog factory 的 `optionRule()` 将某些选项声明为 `required`,而传入
`createOptionalCatalog()` 的配置中这些选项并不总是存在,则会抛出
`OptionValidationException`。这主要影响通过 `JdbcCatalogUtils.findCatalog()` 触发的 JDBC
连接器路径。
+ - **迁移指南**:如果您有自定义的 `CatalogFactory` 实现,请确保其 `optionRule()`
准确反映在运行时到达它的配置中,哪些选项是真正必填的,哪些是可选的。
+
+
### 连接器变更
- **破坏性变更:Iceberg 连接器 — 不再自动继承源表主键**
diff --git
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-sqlserver/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/sqlserver/source/SqlServerIncrementalSourceFactoryTest.java
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-sqlserver/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/sqlserver/source/SqlServerIncrementalSourceFactoryTest.java
index af86c4fc11..e814f76ee5 100644
---
a/seatunnel-connectors-v2/connector-cdc/connector-cdc-sqlserver/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/sqlserver/source/SqlServerIncrementalSourceFactoryTest.java
+++
b/seatunnel-connectors-v2/connector-cdc/connector-cdc-sqlserver/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/sqlserver/source/SqlServerIncrementalSourceFactoryTest.java
@@ -17,12 +17,45 @@
package org.apache.seatunnel.connectors.seatunnel.cdc.sqlserver.source;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.table.catalog.Catalog;
+import org.apache.seatunnel.api.table.factory.FactoryUtil;
+
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
class SqlServerIncrementalSourceFactoryTest {
@Test
public void testOptionRule() {
Assertions.assertNotNull((new
SqlServerIncrementalSourceFactory()).optionRule());
}
+
+ /**
+ * SQLServer CDC source creation must accept the driver-specific
databaseName URL syntax during
+ * the submission-time catalog validation step.
+ */
+ @Test
+ public void testCreateOptionalCatalogWithSqlServerStyleUrl() {
+ Map<String, Object> config = new HashMap<>();
+ config.put("url",
"jdbc:sqlserver://localhost:1433;databaseName=seatunnel");
+ config.put("username", "sa");
+ config.put("password", "Password!");
+ config.put("database-names", Arrays.asList("seatunnel"));
+ config.put("table-names", Arrays.asList("seatunnel.dbo.orders"));
+
+ Optional<Catalog> catalog =
+ FactoryUtil.createOptionalCatalog(
+ "SqlServer",
+ ReadonlyConfig.fromMap(config),
+ Thread.currentThread().getContextClassLoader(),
+ "SqlServer");
+
+ Assertions.assertTrue(catalog.isPresent());
+ catalog.ifPresent(Catalog::close);
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalogFactory.java
index ead206a230..5494d1b147 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalogFactory.java
@@ -51,6 +51,6 @@ public class DamengCatalogFactory implements CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/duckdb/DuckDBCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/duckdb/DuckDBCatalogFactory.java
index 53999ace8f..b6232fe2c5 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/duckdb/DuckDBCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/duckdb/DuckDBCatalogFactory.java
@@ -28,13 +28,6 @@ import
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.DatabaseI
import com.google.auto.service.AutoService;
-import static
org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcCommonOptions.DECIMAL_TYPE_NARROWING;
-import static
org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcCommonOptions.HANDLE_BLOB_AS_STRING;
-import static
org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcCommonOptions.PASSWORD;
-import static
org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcCommonOptions.SCHEMA;
-import static
org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcCommonOptions.URL;
-import static
org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcCommonOptions.USERNAME;
-
/** Factory for {@link DuckDBCatalog} */
@AutoService(Factory.class)
public class DuckDBCatalogFactory implements CatalogFactory {
@@ -58,8 +51,13 @@ public class DuckDBCatalogFactory implements CatalogFactory {
@Override
public OptionRule optionRule() {
return OptionRule.builder()
- .required(URL)
- .optional(USERNAME, PASSWORD, SCHEMA, DECIMAL_TYPE_NARROWING,
HANDLE_BLOB_AS_STRING)
+ .required(JdbcCommonOptions.URL)
+ .optional(
+ JdbcCommonOptions.USERNAME,
+ JdbcCommonOptions.PASSWORD,
+ JdbcCommonOptions.SCHEMA,
+ JdbcCommonOptions.DECIMAL_TYPE_NARROWING,
+ JdbcCommonOptions.HANDLE_BLOB_AS_STRING)
.build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/highgo/HighGoCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/highgo/HighGoCatalogFactory.java
index e612541075..caa8e59b10 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/highgo/HighGoCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/highgo/HighGoCatalogFactory.java
@@ -45,6 +45,6 @@ public class HighGoCatalogFactory implements CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/iris/IrisCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/iris/IrisCatalogFactory.java
index 33ed340f27..e951b07695 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/iris/IrisCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/iris/IrisCatalogFactory.java
@@ -50,6 +50,6 @@ public class IrisCatalogFactory implements CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/kingbase/KingbaseCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/kingbase/KingbaseCatalogFactory.java
index b535ce5f50..9453115d13 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/kingbase/KingbaseCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/kingbase/KingbaseCatalogFactory.java
@@ -51,6 +51,6 @@ public class KingbaseCatalogFactory implements CatalogFactory
{
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/mysql/MySqlCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/mysql/MySqlCatalogFactory.java
index 052f8763ab..537b8c768a 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/mysql/MySqlCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/mysql/MySqlCatalogFactory.java
@@ -51,6 +51,6 @@ public class MySqlCatalogFactory implements CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalogFactory.java
index c80b163e74..bffb1800bf 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/opengauss/OpenGaussCatalogFactory.java
@@ -51,6 +51,6 @@ public class OpenGaussCatalogFactory implements
CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/oracle/OracleCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/oracle/OracleCatalogFactory.java
index 782130bd96..bd2794a751 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/oracle/OracleCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/oracle/OracleCatalogFactory.java
@@ -17,8 +17,12 @@
package org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.oracle;
+import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils;
+
import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConditionExtension;
import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
import org.apache.seatunnel.api.table.catalog.Catalog;
import org.apache.seatunnel.api.table.factory.CatalogFactory;
import org.apache.seatunnel.api.table.factory.Factory;
@@ -53,6 +57,31 @@ public class OracleCatalogFactory implements CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule(new
OracleUrlValidator()).build();
+ }
+
+ static class OracleUrlValidator implements ConditionExtension<String> {
+ @Override
+ public String description() {
+ return "Oracle JDBC URL must contain a service name (e.g.
jdbc:oracle:thin:@host:port/service)";
+ }
+
+ @Override
+ public boolean evaluate(ReadonlyConfig config, String url) {
+ if (url == null || url.trim().isEmpty()) {
+ return false;
+ }
+ try {
+ JdbcUrlUtil.UrlInfo info = OracleURLParser.parse(url);
+ return StringUtils.isNotBlank(info.getHost())
+ && info.getDefaultDatabase().isPresent();
+ } catch (IllegalArgumentException e) {
+ throw new OptionValidationException(
+ String.format(
+ "Invalid Oracle JDBC URL format: [%s], "
+ + "expected pattern:
jdbc:oracle:thin:@host:port/service",
+ url));
+ }
+ }
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCatalogFactory.java
index 7217432eb6..3215eace02 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/psql/PostgresCatalogFactory.java
@@ -51,6 +51,6 @@ public class PostgresCatalogFactory implements CatalogFactory
{
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/redshift/RedshiftCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/redshift/RedshiftCatalogFactory.java
index 7931ef3826..728bc48977 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/redshift/RedshiftCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/redshift/RedshiftCatalogFactory.java
@@ -51,6 +51,6 @@ public class RedshiftCatalogFactory implements CatalogFactory
{
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/saphana/SapHanaCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/saphana/SapHanaCatalogFactory.java
index ba7dd361b6..0e1f73abae 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/saphana/SapHanaCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/saphana/SapHanaCatalogFactory.java
@@ -17,8 +17,12 @@
package org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.saphana;
+import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils;
+
import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConditionExtension;
import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
import org.apache.seatunnel.api.table.catalog.Catalog;
import org.apache.seatunnel.api.table.factory.CatalogFactory;
import org.apache.seatunnel.api.table.factory.Factory;
@@ -51,6 +55,30 @@ public class SapHanaCatalogFactory implements CatalogFactory
{
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule(new
SapHanaUrlValidator()).build();
+ }
+
+ static class SapHanaUrlValidator implements ConditionExtension<String> {
+ @Override
+ public String description() {
+ return "SAP HANA JDBC URL must be a valid format (e.g.
jdbc:sap://host:port)";
+ }
+
+ @Override
+ public boolean evaluate(ReadonlyConfig config, String url) {
+ if (url == null || url.trim().isEmpty()) {
+ return false;
+ }
+ try {
+ JdbcUrlUtil.UrlInfo info = SapHanaURLParser.parse(url);
+ return StringUtils.isNotBlank(info.getHost());
+ } catch (IllegalArgumentException e) {
+ throw new OptionValidationException(
+ String.format(
+ "Invalid SAP HANA JDBC URL format: [%s], "
+ + "expected pattern:
jdbc:sap://host:port",
+ url));
+ }
+ }
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/saphana/SapHanaURLParser.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/saphana/SapHanaURLParser.java
index 6c6e66fbfe..704947d481 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/saphana/SapHanaURLParser.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/saphana/SapHanaURLParser.java
@@ -25,7 +25,8 @@ import java.util.regex.Pattern;
public class SapHanaURLParser {
private static final Pattern HANA_URL_PATTERN =
-
Pattern.compile("^(?<url>jdbc:sap://(?<host>[^:]+):(?<port>\\d+)/\\?(?<params>.*?))$");
+ Pattern.compile(
+
"^(?<url>jdbc:sap://(?<host>[^:]+):(?<port>\\d+)(/\\?(?<params>.*?))?)$");
public static JdbcUrlUtil.UrlInfo parse(String url) {
Matcher matcher = HANA_URL_PATTERN.matcher(url);
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/sqlserver/SqlServerCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/sqlserver/SqlServerCatalogFactory.java
index aa345997d2..0624347c3e 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/sqlserver/SqlServerCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/sqlserver/SqlServerCatalogFactory.java
@@ -17,8 +17,12 @@
package org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.sqlserver;
+import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils;
+
import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConditionExtension;
import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
import org.apache.seatunnel.api.table.catalog.Catalog;
import org.apache.seatunnel.api.table.factory.CatalogFactory;
import org.apache.seatunnel.api.table.factory.Factory;
@@ -51,6 +55,31 @@ public class SqlServerCatalogFactory implements
CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule(new
SqlServerUrlValidator()).build();
+ }
+
+ /** Validates URL format only; database may be provided via separate
config option. */
+ static class SqlServerUrlValidator implements ConditionExtension<String> {
+ @Override
+ public String description() {
+ return "SqlServer JDBC URL must be a valid format (e.g.
jdbc:sqlserver://host:port;databaseName=db)";
+ }
+
+ @Override
+ public boolean evaluate(ReadonlyConfig config, String url) {
+ if (url == null || url.trim().isEmpty()) {
+ return false;
+ }
+ try {
+ JdbcUrlUtil.UrlInfo info = SqlServerURLParser.parse(url);
+ return info != null && StringUtils.isNotBlank(info.getHost());
+ } catch (IllegalArgumentException e) {
+ throw new OptionValidationException(
+ String.format(
+ "Invalid SqlServer JDBC URL format: [%s], "
+ + "expected pattern:
jdbc:sqlserver://host:port[;databaseName=db]",
+ url));
+ }
+ }
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/tidb/TiDBCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/tidb/TiDBCatalogFactory.java
index beb2945b87..153062851e 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/tidb/TiDBCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/tidb/TiDBCatalogFactory.java
@@ -50,6 +50,6 @@ public class TiDBCatalogFactory implements CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/xugu/XuguCatalogFactory.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/xugu/XuguCatalogFactory.java
index 63f35bfd35..cd239e7e9b 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/xugu/XuguCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/xugu/XuguCatalogFactory.java
@@ -23,7 +23,6 @@ import org.apache.seatunnel.api.table.catalog.Catalog;
import org.apache.seatunnel.api.table.factory.CatalogFactory;
import org.apache.seatunnel.api.table.factory.Factory;
import org.apache.seatunnel.common.utils.JdbcUrlUtil;
-import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.oracle.OracleURLParser;
import org.apache.seatunnel.connectors.seatunnel.jdbc.config.JdbcCommonOptions;
import
org.apache.seatunnel.connectors.seatunnel.jdbc.internal.dialect.DatabaseIdentifier;
@@ -40,7 +39,7 @@ public class XuguCatalogFactory implements CatalogFactory {
@Override
public Catalog createCatalog(String catalogName, ReadonlyConfig options) {
String urlWithDatabase = options.get(JdbcCommonOptions.URL);
- JdbcUrlUtil.UrlInfo urlInfo = OracleURLParser.parse(urlWithDatabase);
+ JdbcUrlUtil.UrlInfo urlInfo = JdbcUrlUtil.getUrlInfo(urlWithDatabase);
return new XuguCatalog(
catalogName,
options.get(JdbcCommonOptions.USERNAME),
@@ -52,6 +51,6 @@ public class XuguCatalogFactory implements CatalogFactory {
@Override
public OptionRule optionRule() {
- return JdbcCommonOptions.BASE_CATALOG_RULE.build();
+ return JdbcCommonOptions.baseCatalogRule().build();
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/config/JdbcCommonOptions.java
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/config/JdbcCommonOptions.java
index 2c636be5c4..bff276da22 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/config/JdbcCommonOptions.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/config/JdbcCommonOptions.java
@@ -17,12 +17,15 @@
package org.apache.seatunnel.connectors.seatunnel.jdbc.config;
+import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils;
+
import org.apache.seatunnel.api.configuration.Option;
import org.apache.seatunnel.api.configuration.Options;
import org.apache.seatunnel.api.configuration.ReadonlyConfig;
import org.apache.seatunnel.api.configuration.util.ConditionExtension;
import org.apache.seatunnel.api.configuration.util.Conditions;
import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
import org.apache.seatunnel.common.utils.JdbcUrlUtil;
import java.util.Map;
@@ -162,38 +165,42 @@ public class JdbcCommonOptions {
public static final Option<String> REGION =
Options.key("region").stringType().noDefaultValue().withDescription("region");
- /** @deprecated Use {@link #baseCatalogRule()} instead to avoid shared
mutable state. */
- @Deprecated public static final OptionRule.Builder BASE_CATALOG_RULE =
baseCatalogRule();
-
/**
- * Returns a fresh {@link OptionRule.Builder} with the base validation
rules shared by all JDBC
- * catalog factories (MySQL, PostgreSQL, Oracle, etc.).
+ * Returns a fresh {@link OptionRule.Builder} with the base validation
rules shared by standard
+ * JDBC catalog factories (MySQL, PostgreSQL, etc.) that use generic
{@code host:port/database}
+ * URL format and require username/password authentication.
*
- * <p>These rules are evaluated at <b>submission time</b> via {@code
- * ConfigValidator.validate(factory.optionRule())} in the {@code
FactoryUtil} entry path. They
- * enforce that the JDBC URL contains a database name, and that
username/password are provided.
+ * <p>Catalog factories with non-standard URL formats (SqlServer, Oracle,
SapHana) should use
+ * {@link #baseCatalogRule(ConditionExtension)} with their own URL
validator.
*
- * <p>Individual catalog factories may append additional rules (e.g.
OceanBase requires {@code
- * compatible_mode}) before calling {@code .build()}.
+ * <p>Catalog factories that do not require authentication (e.g. DuckDB)
should define their own
+ * {@code optionRule()} directly.
*/
public static OptionRule.Builder baseCatalogRule() {
+ return baseCatalogRule(new UrlContainsDatabaseValidator());
+ }
+
+ /**
+ * Returns a fresh {@link OptionRule.Builder} with a custom URL validator.
Use this for
+ * databases whose JDBC URL does not follow the standard {@code
host:port/database} format (e.g.
+ * SqlServer, Oracle, SapHana).
+ */
+ public static OptionRule.Builder
baseCatalogRule(ConditionExtension<String> urlValidator) {
return OptionRule.builder()
- .required(URL, Conditions.extension(URL, new
UrlContainsDatabaseValidator()))
+ .required(URL, Conditions.extension(URL, urlValidator))
.required(USERNAME, PASSWORD)
.optional(SCHEMA, DECIMAL_TYPE_NARROWING,
HANDLE_BLOB_AS_STRING);
}
/**
- * Submission-time validator that ensures the JDBC URL contains a database
name.
- *
- * <p>This validator is attached to the {@code url} option via {@link
- * Conditions#extension(Option, ConditionExtension)} and is evaluated by
{@code ConfigValidator}
- * before the catalog/source/sink factory creates its connector instance.
+ * Validates that the JDBC URL has a valid format with at least a host
component. Database name
+ * is optional to maintain backward compatibility with connectors (e.g.
StarRocks, Doris) that
+ * specify the database in the query or table_path instead of the URL.
*/
public static class UrlContainsDatabaseValidator implements
ConditionExtension<String> {
@Override
public String description() {
- return "JDBC URL must contain a database name";
+ return "JDBC URL must be a valid format:
jdbc:<scheme>://host:port[/database]";
}
@Override
@@ -202,9 +209,14 @@ public class JdbcCommonOptions {
return false;
}
try {
- return
JdbcUrlUtil.getUrlInfo(url).getDefaultDatabase().isPresent();
+ JdbcUrlUtil.UrlInfo urlInfo = JdbcUrlUtil.getUrlInfo(url);
+ return StringUtils.isNotBlank(urlInfo.getHost());
} catch (IllegalArgumentException e) {
- return false;
+ throw new OptionValidationException(
+ String.format(
+ "Invalid JDBC URL format: [%s], "
+ + "expected pattern:
jdbc:<scheme>://host:port[/database]",
+ url));
}
}
}
diff --git
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/JdbcCatalogFactoryTest.java
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/JdbcCatalogFactoryTest.java
index 5aadf30058..2b827e3248 100644
---
a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/JdbcCatalogFactoryTest.java
+++
b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/JdbcCatalogFactoryTest.java
@@ -21,9 +21,14 @@ import org.apache.seatunnel.api.configuration.ReadonlyConfig;
import org.apache.seatunnel.api.configuration.util.ConfigValidator;
import org.apache.seatunnel.api.configuration.util.OptionRule;
import org.apache.seatunnel.api.configuration.util.OptionValidationException;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.dm.DamengCatalogFactory;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.duckdb.DuckDBCatalogFactory;
import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.mysql.MySqlCatalogFactory;
import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.oceanbase.OceanBaseCatalogFactory;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.oracle.OracleCatalogFactory;
import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.psql.PostgresCatalogFactory;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.saphana.SapHanaCatalogFactory;
+import
org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.sqlserver.SqlServerCatalogFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -40,6 +45,8 @@ class JdbcCatalogFactoryTest {
ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(rule);
}
+ // ==================== Standard URL validators (MySQL / Postgres)
====================
+
@Test
void testValidCatalogConfig() {
Map<String, Object> cfg = new HashMap<>();
@@ -68,39 +75,42 @@ class JdbcCatalogFactoryTest {
}
@Test
- void testUrlWithoutDatabaseFails() {
+ void testBlankUrlFails() {
Map<String, Object> cfg = new HashMap<>();
- cfg.put("url", "jdbc:mysql://host:3306");
+ cfg.put("url", "");
cfg.put("username", "root");
cfg.put("password", "pass");
Assertions.assertThrows(OptionValidationException.class, () ->
validate(mysqlRule, cfg));
}
@Test
- void testBlankUrlFails() {
+ void testMissingCredentialsFails() {
Map<String, Object> cfg = new HashMap<>();
- cfg.put("url", "");
- cfg.put("username", "root");
- cfg.put("password", "pass");
+ cfg.put("url", "jdbc:mysql://localhost:3306/mydb");
Assertions.assertThrows(OptionValidationException.class, () ->
validate(mysqlRule, cfg));
}
@Test
- void testMissingUsernameFails() {
+ void testMissingPasswordFails() {
Map<String, Object> cfg = new HashMap<>();
- cfg.put("url", "jdbc:mysql://host:3306/mydb");
- cfg.put("password", "pass");
+ cfg.put("url", "jdbc:mysql://localhost:3306/mydb");
+ cfg.put("username", "root");
Assertions.assertThrows(OptionValidationException.class, () ->
validate(mysqlRule, cfg));
}
@Test
- void testMissingPasswordFails() {
+ void testCatalogConfigMimicsExtractCatalogConfig() {
Map<String, Object> cfg = new HashMap<>();
- cfg.put("url", "jdbc:mysql://host:3306/mydb");
+ cfg.put("url", "jdbc:mysql://localhost:3306/mydb");
cfg.put("username", "root");
- Assertions.assertThrows(OptionValidationException.class, () ->
validate(mysqlRule, cfg));
+ cfg.put("password", "pass");
+ cfg.put("decimal_type_narrowing", true);
+ cfg.put("handle_blob_as_string", false);
+ Assertions.assertDoesNotThrow(() -> validate(mysqlRule, cfg));
}
+ // ==================== OceanBase ====================
+
@Test
void testOceanBaseWithoutCompatibleModeFails() {
OptionRule obRule = new OceanBaseCatalogFactory().optionRule();
@@ -110,4 +120,92 @@ class JdbcCatalogFactoryTest {
cfg.put("password", "pass");
Assertions.assertThrows(OptionValidationException.class, () ->
validate(obRule, cfg));
}
+
+ @Test
+ void testOceanBaseValidConfig() {
+ OptionRule obRule = new OceanBaseCatalogFactory().optionRule();
+ Map<String, Object> cfg = new HashMap<>();
+ cfg.put("url", "jdbc:oceanbase://localhost:2881/mydb");
+ cfg.put("username", "root");
+ cfg.put("password", "pass");
+ cfg.put("compatible_mode", "mysql");
+ Assertions.assertDoesNotThrow(() -> validate(obRule, cfg));
+ }
+
+ // ==================== Dameng (no database in URL) ====================
+
+ @Test
+ void testDamengCatalogUrlWithoutDatabase() {
+ OptionRule dmRule = new DamengCatalogFactory().optionRule();
+ Map<String, Object> cfg = new HashMap<>();
+ cfg.put("url", "jdbc:dm://e2e_dmdb:5236");
+ cfg.put("username", "SYSDBA");
+ cfg.put("password", "SYSDBA");
+ Assertions.assertDoesNotThrow(() -> validate(dmRule, cfg));
+ }
+
+ // ==================== DuckDB (no credentials required)
====================
+
+ @Test
+ void testDuckDBCatalogConfigNoCredentials() {
+ OptionRule rule = new DuckDBCatalogFactory().optionRule();
+ Map<String, Object> cfg = new HashMap<>();
+ cfg.put("url", "jdbc:duckdb:/tmp/test.db");
+ Assertions.assertDoesNotThrow(() -> validate(rule, cfg));
+ }
+
+ // ==================== SqlServer dialect validator ====================
+
+ @Test
+ void testSqlServerCatalogUrlWithDatabaseNameProperty() {
+ Map<String, Object> cfg = new HashMap<>();
+ cfg.put("url",
"jdbc:sqlserver://localhost:1433;databaseName=seatunnel");
+ cfg.put("username", "sa");
+ cfg.put("password", "Password!");
+ Assertions.assertDoesNotThrow(
+ () -> validate(new SqlServerCatalogFactory().optionRule(),
cfg));
+ }
+
+ @Test
+ void testSqlServerCatalogUrlWithoutDatabasePasses() {
+ Map<String, Object> cfg = new HashMap<>();
+ cfg.put("url", "jdbc:sqlserver://localhost:1433;encrypt=false");
+ cfg.put("username", "sa");
+ cfg.put("password", "Password!");
+ Assertions.assertDoesNotThrow(
+ () -> validate(new SqlServerCatalogFactory().optionRule(),
cfg));
+ }
+
+ // ==================== Oracle dialect validator ====================
+
+ @Test
+ void testOracleCatalogThinUrlWithoutDoubleSlash() {
+ Map<String, Object> cfg = new HashMap<>();
+ cfg.put("url", "jdbc:oracle:thin:@localhost:1521/ORCLCDB");
+ cfg.put("username", "system");
+ cfg.put("password", "oracle");
+ Assertions.assertDoesNotThrow(() -> validate(new
OracleCatalogFactory().optionRule(), cfg));
+ }
+
+ // ==================== SapHana dialect validator ====================
+
+ @Test
+ void testSapHanaCatalogHostOnlyUrl() {
+ Map<String, Object> cfg = new HashMap<>();
+ cfg.put("url", "jdbc:sap://localhost:39017");
+ cfg.put("username", "SYSTEM");
+ cfg.put("password", "Password1");
+ Assertions.assertDoesNotThrow(
+ () -> validate(new SapHanaCatalogFactory().optionRule(), cfg));
+ }
+
+ @Test
+ void testSapHanaCatalogWithDatabaseParam() {
+ Map<String, Object> cfg = new HashMap<>();
+ cfg.put("url", "jdbc:sap://localhost:39017/?databaseName=HXE");
+ cfg.put("username", "SYSTEM");
+ cfg.put("password", "Password1");
+ Assertions.assertDoesNotThrow(
+ () -> validate(new SapHanaCatalogFactory().optionRule(), cfg));
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-lance/src/main/java/org/apache/seatunnel/connectors/seatunnel/lance/catalog/LanceCatalogFactory.java
b/seatunnel-connectors-v2/connector-lance/src/main/java/org/apache/seatunnel/connectors/seatunnel/lance/catalog/LanceCatalogFactory.java
index 7d26b4e8ef..a96529bcd6 100644
---
a/seatunnel-connectors-v2/connector-lance/src/main/java/org/apache/seatunnel/connectors/seatunnel/lance/catalog/LanceCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-lance/src/main/java/org/apache/seatunnel/connectors/seatunnel/lance/catalog/LanceCatalogFactory.java
@@ -21,7 +21,11 @@ import org.apache.seatunnel.api.configuration.ReadonlyConfig;
import org.apache.seatunnel.api.configuration.util.OptionRule;
import org.apache.seatunnel.api.table.catalog.Catalog;
import org.apache.seatunnel.api.table.factory.CatalogFactory;
+import org.apache.seatunnel.api.table.factory.Factory;
+import com.google.auto.service.AutoService;
+
+@AutoService(Factory.class)
public class LanceCatalogFactory implements CatalogFactory {
@Override
public Catalog createCatalog(String catalogName, ReadonlyConfig
readonlyConfig) {