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 57c1a39b5a [Feature][Connector-V2] PR2:Support table_options for
StarRocks SaveMode auto-create (#11242)
57c1a39b5a is described below
commit 57c1a39b5a60f846708840233c009c6200cfa5dd
Author: luxiaolong <[email protected]>
AuthorDate: Sun Jul 12 19:43:43 2026 +0800
[Feature][Connector-V2] PR2:Support table_options for StarRocks SaveMode
auto-create (#11242)
Co-authored-by: det101 <[email protected]>
Co-authored-by: Cursor <[email protected]>
---
docs/en/connectors/sink/StarRocks.md | 30 ++++
docs/zh/connectors/sink/StarRocks.md | 30 ++++
.../seatunnel/common/sql/ClauseMergeFormat.java | 35 ++++
.../seatunnel/common/sql/SqlTableClauseMerger.java | 196 +++++++++++++++++++++
.../common/sql/SqlTableClauseMergerTest.java | 90 ++++++++++
.../starrocks/catalog/StarRocksCatalog.java | 18 +-
.../starrocks/catalog/StarRocksCatalogFactory.java | 4 +-
.../starrocks/sink/StarRocksSaveModeUtil.java | 69 ++++++++
.../seatunnel/starrocks/sink/StarRocksSink.java | 9 +-
.../starrocks/sink/StarRocksSinkFactory.java | 15 +-
.../StarRocksTableOptionsConditionExtension.java | 58 ++++++
.../sink/StarRocksTableOptionsValidator.java | 41 +++++
.../starrocks/catalog/PreviewActionTest.java | 40 +++++
.../catalog/StarRocksCreateTableTest.java | 32 ++++
.../StarRocksSaveModeUtilTableOptionsTest.java | 74 ++++++++
...tarRocksTableOptionsConditionExtensionTest.java | 88 +++++++++
.../e2e/connector/starrocks/StarRocksIT.java | 45 +++++
.../fake-to-starrocks-with-table-options.conf | 74 ++++++++
18 files changed, 941 insertions(+), 7 deletions(-)
diff --git a/docs/en/connectors/sink/StarRocks.md
b/docs/en/connectors/sink/StarRocks.md
index 90ad3197a1..e53d77f2cf 100644
--- a/docs/en/connectors/sink/StarRocks.md
+++ b/docs/en/connectors/sink/StarRocks.md
@@ -54,6 +54,7 @@ The internal implementation of StarRocks sink connector is
cached and imported b
| http_socket_timeout_ms | int | no | 180000
| Set http socket timeout, default is 3 minutes.
|
| schema_save_mode | Enum | no |
CREATE_SCHEMA_WHEN_NOT_EXIST | Before the synchronous task is turned on,
different treatment schemes are selected for the existing surface structure of
the target side.
|
| data_save_mode | Enum | no | APPEND_DATA
| Before the synchronous task is turned on, different processing schemes are
selected for data existing data on the target side.
|
+| table_options | Map | no | -
| Sink-specific table properties merged into CREATE TABLE PROPERTIES during
SaveMode auto-create. See below.
|
| custom_sql | String | no | -
| When data_save_mode selects CUSTOM_PROCESSING, you should fill in the
CUSTOM_SQL parameter. This parameter usually fills in a SQL that can be
executed. SQL will be executed before synchronization tasks. |
### save_mode_create_template
@@ -140,6 +141,35 @@ Option introduction:
When data_save_mode selects CUSTOM_PROCESSING, you should fill in the
CUSTOM_SQL parameter. This parameter usually fills in a SQL that can be
executed. SQL will be executed before synchronization tasks.
+### table_options [Map]
+
+Sink-specific table options applied when SaveMode auto-creates the target
table (DDL phase). They take effect only when `schema_save_mode` triggers table
creation, such as `CREATE_SCHEMA_WHEN_NOT_EXIST` or `RECREATE_SCHEMA`. They do
**not** affect Stream Load writes at runtime and do **not** run `ALTER TABLE`
on existing tables.
+
+When used with the default `save_mode_create_template` (option omitted, or
configured with the same content as the built-in default), `table_options` are
merged into the template `PROPERTIES` clause. **Duplicate keys are overridden
by `table_options`.** Use property names from the [StarRocks CREATE TABLE
documentation](https://docs.starrocks.io/docs/sql-reference/sql-statements/table_bucket/partition/CREATE_TABLE/#properties);
SeaTunnel does not maintain an allowlist—invalid properties f [...]
+
+If you configure a `save_mode_create_template` that **differs from the
built-in default**, `table_options` cannot be used together (validation fails
at job submission). Put properties directly in the template instead.
+
+Invalid combinations are validated early via `StarRocksSinkFactory` option
rules (`--check` and job submission), not only when StarRocks executes CREATE
TABLE.
+
+Example:
+
+```hocon
+sink {
+ StarRocks {
+ base-url = "jdbc:mysql://127.0.0.1:9030"
+ nodeUrls = ["127.0.0.1:8030"]
+ username = "root"
+ password = ""
+ database = "test"
+ schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"
+ table_options = {
+ replication_num = "3"
+ storage_format = "V2"
+ }
+ }
+}
+```
+
## Data Type Mapping
| StarRocks Data type | SeaTunnel Data type |
diff --git a/docs/zh/connectors/sink/StarRocks.md
b/docs/zh/connectors/sink/StarRocks.md
index 8fc68e0e29..36b34067f1 100644
--- a/docs/zh/connectors/sink/StarRocks.md
+++ b/docs/zh/connectors/sink/StarRocks.md
@@ -53,6 +53,7 @@ StarRocks数据接收器内部实现采用了缓存,通过stream load将数据
| http_socket_timeout_ms | int | no | 180000
| http socket超时时间,默认为3分钟
|
| schema_save_mode | Enum | no | CREATE_SCHEMA_WHEN_NOT_EXIST
| 在同步任务打开之前,针对目标端已存在的表结构选择不同的处理方法
|
| data_save_mode | Enum | no | APPEND_DATA
| 在同步任务打开之前,针对目标端已存在的数据选择不同的处理方法
|
+| table_options | Map | no | -
| SaveMode 自动建表时合并进 CREATE TABLE PROPERTIES 的表级属性,详见下文
|
| custom_sql | String | no | -
|
当data_save_mode设置为CUSTOM_PROCESSING时,必须同时设置CUSTOM_SQL参数。CUSTOM_SQL的值为可执行的SQL语句,在同步任务开启前SQL将会被执行
|
### save_mode_create_template
@@ -134,6 +135,35 @@ table选项参数可以填入一任意表名,这个名字最终会被用作目
当data_save_mode设置为CUSTOM_PROCESSING时,必须同时设置CUSTOM_SQL参数。CUSTOM_SQL的值为可执行的SQL语句,在同步任务开启前SQL将会被执行。
+### table_options [Map]
+
+Sink 在 SaveMode 自动建表(DDL)时附加的表级属性。仅在 `schema_save_mode` 触发建表时生效,例如
`CREATE_SCHEMA_WHEN_NOT_EXIST`、`RECREATE_SCHEMA`;**不影响** Stream Load
写入,也**不会**对已存在表执行 `ALTER TABLE`。
+
+在默认 `save_mode_create_template`(未配置或与内置默认值相同)下,`table_options` 会合并进模板
`PROPERTIES` 子句;**同名 key 以 `table_options` 为准**。属性名请参考 [StarRocks CREATE TABLE
文档](https://docs.starrocks.io/docs/sql-reference/sql-statements/table_bucket/partition/CREATE_TABLE/#properties);SeaTunnel
不做白名单,非法属性由 StarRocks 执行 DDL 时报错。
+
+若配置了**与内置默认值不同**的 `save_mode_create_template`,则不能与 `table_options`
同时使用(任务提交时校验失败);此时请将属性直接写入模板。
+
+非法组合会在 `StarRocksSinkFactory` 的 option 规则阶段提前校验(`--check` 与作业提交),而非仅在
StarRocks 执行 CREATE TABLE 时失败。
+
+示例:
+
+```hocon
+sink {
+ StarRocks {
+ base-url = "jdbc:mysql://127.0.0.1:9030"
+ nodeUrls = ["127.0.0.1:8030"]
+ username = "root"
+ password = ""
+ database = "test"
+ schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"
+ table_options = {
+ replication_num = "3"
+ storage_format = "V2"
+ }
+ }
+}
+```
+
## 数据类型映射
| StarRocks数据类型 | SeaTunnel数据类型 |
diff --git
a/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/sql/ClauseMergeFormat.java
b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/sql/ClauseMergeFormat.java
new file mode 100644
index 0000000000..a8f8f477a9
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/sql/ClauseMergeFormat.java
@@ -0,0 +1,35 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.common.sql;
+
+/** Format definition for SQL table property clauses merged by {@link
SqlTableClauseMerger}. */
+public enum ClauseMergeFormat {
+
+ /** StarRocks/Doris style: {@code PROPERTIES ("key" = "value")}. */
+ DOUBLE_QUOTED_PROPERTIES("PROPERTIES");
+
+ private final String keyword;
+
+ ClauseMergeFormat(String keyword) {
+ this.keyword = keyword;
+ }
+
+ public String getKeyword() {
+ return keyword;
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/sql/SqlTableClauseMerger.java
b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/sql/SqlTableClauseMerger.java
new file mode 100644
index 0000000000..532f22cc88
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-common/src/main/java/org/apache/seatunnel/connectors/seatunnel/common/sql/SqlTableClauseMerger.java
@@ -0,0 +1,196 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.common.sql;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Utility for merging key-value table options into SQL CREATE TABLE property
clauses. */
+public final class SqlTableClauseMerger {
+
+ private static final Pattern DOUBLE_QUOTED_ENTRY_PATTERN =
+
Pattern.compile("\"((?:[^\"\\\\]|\\\\.)*)\"\\s*=\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
+
+ private SqlTableClauseMerger() {}
+
+ public static String merge(
+ String sql, ClauseMergeFormat format, Map<String, String>
tableOptions) {
+ if (tableOptions == null || tableOptions.isEmpty()) {
+ return sql;
+ }
+ switch (format) {
+ case DOUBLE_QUOTED_PROPERTIES:
+ return mergeDoubleQuotedProperties(sql, format.getKeyword(),
tableOptions);
+ default:
+ throw new IllegalArgumentException("Unsupported clause merge
format: " + format);
+ }
+ }
+
+ private static String mergeDoubleQuotedProperties(
+ String sql, String keyword, Map<String, String> tableOptions) {
+ int keywordStart = findLastKeywordPosition(sql, keyword);
+ if (keywordStart >= 0) {
+ int openParen = indexOfNonWhitespace(sql, keywordStart +
keyword.length());
+ if (openParen >= 0 && sql.charAt(openParen) == '(') {
+ int closeParen = findMatchingCloseParen(sql, openParen);
+ if (closeParen > openParen) {
+ String clauseBody = sql.substring(openParen + 1,
closeParen);
+ Map<String, String> merged =
parseDoubleQuotedEntries(clauseBody);
+ merged.putAll(tableOptions);
+ String newClause = renderDoubleQuotedProperties(keyword,
merged);
+ return sql.substring(0, keywordStart)
+ + newClause
+ + sql.substring(closeParen + 1);
+ }
+ }
+ }
+ return appendDoubleQuotedProperties(sql, keyword, tableOptions);
+ }
+
+ private static String appendDoubleQuotedProperties(
+ String sql, String keyword, Map<String, String> tableOptions) {
+ String trimmed = sql.trim();
+ boolean endsWithSemicolon = trimmed.endsWith(";");
+ if (endsWithSemicolon) {
+ trimmed = trimmed.substring(0, trimmed.length() - 1).trim();
+ }
+ StringBuilder builder = new StringBuilder(trimmed);
+ if (!trimmed.isEmpty()) {
+ builder.append('\n');
+ }
+ builder.append(renderDoubleQuotedProperties(keyword, tableOptions));
+ if (endsWithSemicolon) {
+ builder.append(';');
+ }
+ return builder.toString();
+ }
+
+ private static String renderDoubleQuotedProperties(
+ String keyword, Map<String, String> properties) {
+ StringBuilder builder = new StringBuilder(keyword).append(" (\n");
+ boolean first = true;
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ if (!first) {
+ builder.append(",\n");
+ }
+ builder.append(" \"")
+ .append(escapeDoubleQuoted(entry.getKey()))
+ .append("\" = \"")
+ .append(escapeDoubleQuoted(entry.getValue()))
+ .append('\"');
+ first = false;
+ }
+ builder.append("\n)");
+ return builder.toString();
+ }
+
+ private static Map<String, String> parseDoubleQuotedEntries(String
clauseBody) {
+ Map<String, String> properties = new LinkedHashMap<>();
+ Matcher matcher = DOUBLE_QUOTED_ENTRY_PATTERN.matcher(clauseBody);
+ while (matcher.find()) {
+ properties.put(
+ unescapeDoubleQuoted(matcher.group(1)),
unescapeDoubleQuoted(matcher.group(2)));
+ }
+ return properties;
+ }
+
+ private static int findLastKeywordPosition(String sql, String keyword) {
+ Pattern pattern = Pattern.compile("(?i)\\b" + Pattern.quote(keyword) +
"\\b");
+ Matcher matcher = pattern.matcher(sql);
+ int lastPosition = -1;
+ while (matcher.find()) {
+ lastPosition = matcher.start();
+ }
+ return lastPosition;
+ }
+
+ private static int indexOfNonWhitespace(String sql, int fromIndex) {
+ for (int i = fromIndex; i < sql.length(); i++) {
+ if (!Character.isWhitespace(sql.charAt(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private static int findMatchingCloseParen(String sql, int openParenIndex) {
+ int depth = 0;
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
+ for (int i = openParenIndex; i < sql.length(); i++) {
+ char current = sql.charAt(i);
+ if (inSingleQuote) {
+ if (current == '\'' && !isEscaped(sql, i)) {
+ inSingleQuote = false;
+ }
+ continue;
+ }
+ if (inDoubleQuote) {
+ if (current == '"' && !isEscaped(sql, i)) {
+ inDoubleQuote = false;
+ }
+ continue;
+ }
+ if (current == '\'') {
+ inSingleQuote = true;
+ continue;
+ }
+ if (current == '"') {
+ inDoubleQuote = true;
+ continue;
+ }
+ if (current == '(') {
+ depth++;
+ } else if (current == ')') {
+ depth--;
+ if (depth == 0) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ private static boolean isEscaped(String sql, int index) {
+ int backslashCount = 0;
+ for (int i = index - 1; i >= 0 && sql.charAt(i) == '\\'; i--) {
+ backslashCount++;
+ }
+ return backslashCount % 2 == 1;
+ }
+
+ private static String escapeDoubleQuoted(String value) {
+ return value.replace("\\", "\\\\").replace("\"", "\\\"");
+ }
+
+ private static String unescapeDoubleQuoted(String value) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ char current = value.charAt(i);
+ if (current == '\\' && i + 1 < value.length()) {
+ builder.append(value.charAt(i + 1));
+ i++;
+ } else {
+ builder.append(current);
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/sql/SqlTableClauseMergerTest.java
b/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/sql/SqlTableClauseMergerTest.java
new file mode 100644
index 0000000000..0640f0ab54
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-common/src/test/java/org/apache/seatunnel/connectors/seatunnel/common/sql/SqlTableClauseMergerTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.common.sql;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+class SqlTableClauseMergerTest {
+
+ @Test
+ void mergeOverridesExistingProperty() {
+ String sql =
+ "CREATE TABLE t (\n"
+ + " id INT\n"
+ + ") ENGINE=OLAP\n"
+ + "PROPERTIES (\n"
+ + " \"replication_num\" = \"1\"\n"
+ + ")";
+ Map<String, String> tableOptions = new LinkedHashMap<>();
+ tableOptions.put("replication_num", "3");
+
+ String merged =
+ SqlTableClauseMerger.merge(
+ sql, ClauseMergeFormat.DOUBLE_QUOTED_PROPERTIES,
tableOptions);
+
+ Assertions.assertTrue(merged.contains("\"replication_num\" = \"3\""));
+ Assertions.assertFalse(merged.contains("\"replication_num\" = \"1\""));
+ }
+
+ @Test
+ void mergeAddsNewProperty() {
+ String sql =
+ "CREATE TABLE t (\n"
+ + " id INT\n"
+ + ") ENGINE=OLAP\n"
+ + "PROPERTIES (\n"
+ + " \"replication_num\" = \"1\"\n"
+ + ")";
+ Map<String, String> tableOptions = new LinkedHashMap<>();
+ tableOptions.put("storage_format", "V2");
+
+ String merged =
+ SqlTableClauseMerger.merge(
+ sql, ClauseMergeFormat.DOUBLE_QUOTED_PROPERTIES,
tableOptions);
+
+ Assertions.assertTrue(merged.contains("\"replication_num\" = \"1\""));
+ Assertions.assertTrue(merged.contains("\"storage_format\" = \"V2\""));
+ }
+
+ @Test
+ void appendPropertiesWhenClauseMissing() {
+ String sql = "CREATE TABLE t (id INT) ENGINE=OLAP";
+ Map<String, String> tableOptions = new LinkedHashMap<>();
+ tableOptions.put("replication_num", "3");
+
+ String merged =
+ SqlTableClauseMerger.merge(
+ sql, ClauseMergeFormat.DOUBLE_QUOTED_PROPERTIES,
tableOptions);
+
+ Assertions.assertTrue(merged.contains("PROPERTIES ("));
+ Assertions.assertTrue(merged.contains("\"replication_num\" = \"3\""));
+ }
+
+ @Test
+ void emptyOptionsReturnsOriginalSql() {
+ String sql = "CREATE TABLE t (id INT) ENGINE=OLAP";
+ Assertions.assertEquals(
+ sql,
+ SqlTableClauseMerger.merge(
+ sql, ClauseMergeFormat.DOUBLE_QUOTED_PROPERTIES, new
LinkedHashMap<>()));
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCatalog.java
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCatalog.java
index 1b35a76c3a..5699b45825 100644
---
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCatalog.java
+++
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCatalog.java
@@ -79,12 +79,23 @@ public class StarRocksCatalog implements Catalog {
protected String defaultUrl;
private final JdbcUrlUtil.UrlInfo urlInfo;
private final String template;
+ private final Map<String, String> tableOptions;
private Connection conn;
private static final Logger LOG =
LoggerFactory.getLogger(StarRocksCatalog.class);
public StarRocksCatalog(
String catalogName, String username, String pwd, String
defaultUrl, String template) {
+ this(catalogName, username, pwd, defaultUrl, template,
Collections.emptyMap());
+ }
+
+ public StarRocksCatalog(
+ String catalogName,
+ String username,
+ String pwd,
+ String defaultUrl,
+ String template,
+ Map<String, String> tableOptions) {
checkArgument(StringUtils.isNotBlank(username));
checkArgument(StringUtils.isNotBlank(defaultUrl));
@@ -98,6 +109,7 @@ public class StarRocksCatalog implements Catalog {
this.username = username;
this.pwd = pwd;
this.template = template;
+ this.tableOptions = tableOptions;
}
@Override
@@ -210,7 +222,8 @@ public class StarRocksCatalog implements Catalog {
tablePath.getTableName(),
table.getTableSchema(),
table.getComment(),
- StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key()));
+ StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key(),
+ tableOptions));
}
@Override
@@ -514,7 +527,8 @@ public class StarRocksCatalog implements Catalog {
tablePath.getTableName(),
catalogTable.get().getTableSchema(),
catalogTable.get().getComment(),
-
StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key()));
+
StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key(),
+ tableOptions));
} else if (actionType == ActionType.DROP_TABLE) {
return new SQLPreviewResult(
StarRocksSaveModeUtil.INSTANCE.getDropTableSql(tablePath,
true));
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCatalogFactory.java
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCatalogFactory.java
index 3ad81b68d1..ba06900968 100644
---
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCatalogFactory.java
+++
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCatalogFactory.java
@@ -19,6 +19,7 @@ package
org.apache.seatunnel.connectors.seatunnel.starrocks.catalog;
import org.apache.seatunnel.api.configuration.ReadonlyConfig;
import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.options.SinkConnectorCommonOptions;
import org.apache.seatunnel.api.table.catalog.Catalog;
import org.apache.seatunnel.api.table.factory.CatalogFactory;
import org.apache.seatunnel.api.table.factory.Factory;
@@ -38,7 +39,8 @@ public class StarRocksCatalogFactory implements
CatalogFactory {
options.get(StarRocksSourceOptions.USERNAME),
options.get(StarRocksSourceOptions.PASSWORD),
options.get(StarRocksSinkOptions.BASE_URL),
- options.get(StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE));
+ options.get(StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE),
+ options.get(SinkConnectorCommonOptions.TABLE_OPTIONS));
}
@Override
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtil.java
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtil.java
index 8f160d6c84..80970a2865 100644
---
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtil.java
+++
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtil.java
@@ -19,14 +19,23 @@ package
org.apache.seatunnel.connectors.seatunnel.starrocks.sink;
import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils;
+import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
import org.apache.seatunnel.api.table.catalog.Column;
+import org.apache.seatunnel.api.table.catalog.TableSchema;
import org.apache.seatunnel.api.table.type.ArrayType;
import org.apache.seatunnel.api.table.type.DecimalType;
import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException;
+import org.apache.seatunnel.connectors.seatunnel.common.sql.ClauseMergeFormat;
+import
org.apache.seatunnel.connectors.seatunnel.common.sql.SqlTableClauseMerger;
import org.apache.seatunnel.connectors.seatunnel.common.util.CatalogUtil;
+import
org.apache.seatunnel.connectors.seatunnel.starrocks.config.StarRocksSinkOptions;
import lombok.extern.slf4j.Slf4j;
+import java.util.Map;
+
import static
org.apache.seatunnel.shade.com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@@ -34,6 +43,66 @@ public class StarRocksSaveModeUtil extends CatalogUtil {
public static final StarRocksSaveModeUtil INSTANCE = new
StarRocksSaveModeUtil();
+ private StarRocksSaveModeUtil() {}
+
+ public String getCreateTableSql(
+ String template,
+ String database,
+ String table,
+ TableSchema tableSchema,
+ String comment,
+ String optionsKey,
+ Map<String, String> tableOptions) {
+ String createTableSql =
+ getCreateTableSql(template, database, table, tableSchema,
comment, optionsKey);
+ return applyTableOptionsToCreateTableSql(createTableSql, tableOptions);
+ }
+
+ public void validateTableOptions(ReadonlyConfig config, Map<String,
String> tableOptions) {
+ if (tableOptions == null || tableOptions.isEmpty()) {
+ return;
+ }
+ if (isCustomCreateTemplate(config)) {
+ throw new SeaTunnelRuntimeException(
+ SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
+ "table_options cannot be used together with a custom
save_mode_create_template"
+ + " for StarRocks sink.");
+ }
+ for (Map.Entry<String, String> entry : tableOptions.entrySet()) {
+ if (StringUtils.isBlank(entry.getKey())) {
+ throw new SeaTunnelRuntimeException(
+ SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
+ "table_options contains a blank property key for
StarRocks sink.");
+ }
+ if (entry.getValue() == null) {
+ throw new SeaTunnelRuntimeException(
+ SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
+ String.format(
+ "table_options property '%s' has null value
for StarRocks sink.",
+ entry.getKey()));
+ }
+ }
+ }
+
+ public String applyTableOptionsToCreateTableSql(
+ String createTableSql, Map<String, String> tableOptions) {
+ if (tableOptions == null || tableOptions.isEmpty()) {
+ return createTableSql;
+ }
+ return SqlTableClauseMerger.merge(
+ createTableSql, ClauseMergeFormat.DOUBLE_QUOTED_PROPERTIES,
tableOptions);
+ }
+
+ private static boolean isCustomCreateTemplate(ReadonlyConfig config) {
+ return
config.getOptional(StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE)
+ .map(
+ template ->
+ !template.equals(
+
StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE
+ .defaultValue()))
+ .orElse(false);
+ }
+
public String columnToConnectorType(Column column) {
checkNotNull(column, "The column is required.");
String columnType;
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSink.java
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSink.java
index 1b89ef3f4e..b856ca1e64 100644
---
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSink.java
+++
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSink.java
@@ -41,6 +41,7 @@ import lombok.extern.slf4j.Slf4j;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
@Slf4j
@@ -52,11 +53,14 @@ public class StarRocksSink extends
AbstractSimpleSink<SeaTunnelRow, Void>
private final DataSaveMode dataSaveMode;
private final SchemaSaveMode schemaSaveMode;
private final CatalogTable catalogTable;
+ private final Map<String, String> tableOptions;
- public StarRocksSink(SinkConfig sinkConfig, CatalogTable catalogTable) {
+ public StarRocksSink(
+ SinkConfig sinkConfig, CatalogTable catalogTable, Map<String,
String> tableOptions) {
this.sinkConfig = sinkConfig;
this.tableSchema = catalogTable.getTableSchema();
this.catalogTable = catalogTable;
+ this.tableOptions = tableOptions;
this.dataSaveMode = sinkConfig.getDataSaveMode();
this.schemaSaveMode = sinkConfig.getSchemaSaveMode();
// Load the JDBC driver in to DriverManager
@@ -103,7 +107,8 @@ public class StarRocksSink extends
AbstractSimpleSink<SeaTunnelRow, Void>
sinkConfig.getUsername(),
sinkConfig.getPassword(),
sinkConfig.getJdbcUrl(),
- sinkConfig.getSaveModeCreateTemplate());
+ sinkConfig.getSaveModeCreateTemplate(),
+ tableOptions);
return Optional.of(
new DefaultSaveModeHandler(
schemaSaveMode,
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSinkFactory.java
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSinkFactory.java
index a6efa8bfef..a2b0c4e06d 100644
---
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSinkFactory.java
+++
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSinkFactory.java
@@ -19,7 +19,10 @@ package
org.apache.seatunnel.connectors.seatunnel.starrocks.sink;
import org.apache.seatunnel.shade.org.apache.commons.lang3.StringUtils;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.Conditions;
import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.options.SinkConnectorCommonOptions;
import org.apache.seatunnel.api.sink.DataSaveMode;
import org.apache.seatunnel.api.table.catalog.CatalogTable;
import org.apache.seatunnel.api.table.catalog.TableIdentifier;
@@ -35,6 +38,7 @@ import com.google.auto.service.AutoService;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import static
org.apache.seatunnel.api.options.SinkConnectorCommonOptions.MULTI_TABLE_SINK_REPLICA;
import static
org.apache.seatunnel.connectors.seatunnel.starrocks.config.StarRocksSinkOptions.DATA_SAVE_MODE;
@@ -67,6 +71,11 @@ public class StarRocksSinkFactory implements
TableSinkFactory {
MULTI_TABLE_SINK_REPLICA,
StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE,
StarRocksSinkOptions.HTTP_SOCKET_TIMEOUT_MS)
+ .optional(
+ SinkConnectorCommonOptions.TABLE_OPTIONS,
+ Conditions.extension(
+ SinkConnectorCommonOptions.TABLE_OPTIONS,
+
StarRocksTableOptionsConditionExtension.INSTANCE))
.conditional(
DATA_SAVE_MODE,
DataSaveMode.CUSTOM_PROCESSING,
@@ -81,8 +90,10 @@ public class StarRocksSinkFactory implements
TableSinkFactory {
@Override
public TableSink createSink(TableSinkFactoryContext context) {
+ ReadonlyConfig config = context.getOptions();
+ Map<String, String> sinkTableOptions =
config.get(SinkConnectorCommonOptions.TABLE_OPTIONS);
CatalogTable catalogTable = context.getCatalogTable();
- SinkConfig sinkConfig = SinkConfig.of(context.getOptions());
+ SinkConfig sinkConfig = SinkConfig.of(config);
if (StringUtils.isBlank(sinkConfig.getTable())) {
sinkConfig.setTable(catalogTable.getTableId().getTableName());
}
@@ -101,6 +112,6 @@ public class StarRocksSinkFactory implements
TableSinkFactory {
catalogTable.getPartitionKeys(),
catalogTable.getComment());
- return () -> new StarRocksSink(sinkConfig, finalCatalogTable);
+ return () -> new StarRocksSink(sinkConfig, finalCatalogTable,
sinkTableOptions);
}
}
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsConditionExtension.java
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsConditionExtension.java
new file mode 100644
index 0000000000..7a08d2f051
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsConditionExtension.java
@@ -0,0 +1,58 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.starrocks.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConditionExtension;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
+import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException;
+
+import java.util.Map;
+
+/**
+ * Early validation for StarRocks sink {@code table_options}. Delegates to
{@link
+ * StarRocksTableOptionsValidator}.
+ */
+public class StarRocksTableOptionsConditionExtension
+ implements ConditionExtension<Map<String, String>> {
+
+ public static final StarRocksTableOptionsConditionExtension INSTANCE =
+ new StarRocksTableOptionsConditionExtension();
+
+ private StarRocksTableOptionsConditionExtension() {}
+
+ @Override
+ public String description() {
+ return "must be compatible with the default save_mode_create_template
(see StarRocks"
+ + " connector docs)";
+ }
+
+ @Override
+ public boolean evaluate(ReadonlyConfig config, Map<String, String> value)
+ throws OptionValidationException {
+ if (value == null || value.isEmpty()) {
+ return true;
+ }
+ try {
+ StarRocksTableOptionsValidator.validate(config, value);
+ return true;
+ } catch (SeaTunnelRuntimeException e) {
+ throw new OptionValidationException(e.getMessage());
+ }
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsValidator.java
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsValidator.java
new file mode 100644
index 0000000000..134f4fd544
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-starrocks/src/main/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsValidator.java
@@ -0,0 +1,41 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.starrocks.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.options.SinkConnectorCommonOptions;
+
+import java.util.Collections;
+import java.util.Map;
+
+/** Validates StarRocks sink {@code table_options} before job submission. */
+public final class StarRocksTableOptionsValidator {
+
+ private StarRocksTableOptionsValidator() {}
+
+ public static void validate(ReadonlyConfig config, Map<String, String>
tableOptions) {
+ StarRocksSaveModeUtil.INSTANCE.validateTableOptions(config,
tableOptions);
+ }
+
+ public static void validate(ReadonlyConfig config) {
+ validate(
+ config,
+ config.getOptional(SinkConnectorCommonOptions.TABLE_OPTIONS)
+ .orElse(Collections.emptyMap()));
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/PreviewActionTest.java
b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/PreviewActionTest.java
index ba55560865..022e24f169 100644
---
a/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/PreviewActionTest.java
+++
b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/PreviewActionTest.java
@@ -28,12 +28,14 @@ import
org.apache.seatunnel.api.table.catalog.TableIdentifier;
import org.apache.seatunnel.api.table.catalog.TablePath;
import org.apache.seatunnel.api.table.catalog.TableSchema;
import org.apache.seatunnel.api.table.type.BasicType;
+import
org.apache.seatunnel.connectors.seatunnel.starrocks.config.StarRocksSinkOptions;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Collections;
import java.util.HashMap;
+import java.util.Map;
import java.util.Optional;
public class PreviewActionTest {
@@ -113,6 +115,44 @@ public class PreviewActionTest {
Optional.of(CATALOG_TABLE));
}
+ @Test
+ public void testCreateTablePreviewUsesSinkTableOptionsOnly() {
+ Map<String, String> upstreamOptions = new HashMap<>();
+ upstreamOptions.put("connector", "mysql");
+ upstreamOptions.put("engine", "InnoDB");
+
+ CatalogTable tableWithUpstreamOptions =
+ CatalogTable.of(
+ CATALOG_TABLE.getTableId(),
+ CATALOG_TABLE.getTableSchema(),
+ upstreamOptions,
+ CATALOG_TABLE.getPartitionKeys(),
+ CATALOG_TABLE.getComment());
+
+ Map<String, String> sinkTableOptions = new HashMap<>();
+ sinkTableOptions.put("replication_num", "3");
+
+ StarRocksCatalog catalog =
+ new StarRocksCatalog(
+ "test",
+ "root",
+ "root",
+ "jdbc:mysql://localhost:9030",
+
StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.defaultValue(),
+ sinkTableOptions);
+
+ PreviewResult previewResult =
+ catalog.previewAction(
+ Catalog.ActionType.CREATE_TABLE,
+ TablePath.of("testddatabase.testtable"),
+ Optional.of(tableWithUpstreamOptions));
+
+ String sql = ((SQLPreviewResult) previewResult).getSql();
+ Assertions.assertTrue(sql.contains("\"replication_num\" = \"3\""));
+ Assertions.assertFalse(sql.contains("InnoDB"));
+ Assertions.assertFalse(sql.contains("\"connector\""));
+ }
+
private void assertPreviewResult(
Catalog catalog,
Catalog.ActionType actionType,
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCreateTableTest.java
b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCreateTableTest.java
index ee13e2ac7d..d1cc37003c 100644
---
a/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCreateTableTest.java
+++
b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/catalog/StarRocksCreateTableTest.java
@@ -44,7 +44,9 @@ import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
@Slf4j
public class StarRocksCreateTableTest {
@@ -426,4 +428,34 @@ public class StarRocksCreateTableTest {
+ ")\n",
result);
}
+
+ @Test
+ public void testCreateTableWithTableOptions() {
+ Map<String, String> tableOptions = new HashMap<>();
+ tableOptions.put("replication_num", "3");
+ tableOptions.put("storage_format", "V2");
+
+ String result =
+ StarRocksSaveModeUtil.INSTANCE.getCreateTableSql(
+
StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.defaultValue(),
+ "testdb",
+ "test_table",
+ TableSchema.builder()
+ .column(
+ PhysicalColumn.of(
+ "id",
+ BasicType.INT_TYPE,
+ (Long) null,
+ false,
+ null,
+ ""))
+ .primaryKey(PrimaryKey.of("test",
Collections.singletonList("id")))
+ .build(),
+ "test table",
+ StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key(),
+ tableOptions);
+
+ Assertions.assertTrue(result.contains("\"replication_num\" = \"3\""));
+ Assertions.assertTrue(result.contains("\"storage_format\" = \"V2\""));
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtilTableOptionsTest.java
b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtilTableOptionsTest.java
new file mode 100644
index 0000000000..c64937321c
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksSaveModeUtilTableOptionsTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.starrocks.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException;
+import
org.apache.seatunnel.connectors.seatunnel.starrocks.config.StarRocksSinkOptions;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+class StarRocksSaveModeUtilTableOptionsTest {
+
+ @Test
+ void validateAllowsDefaultTemplateWithTableOptions() {
+ ReadonlyConfig config = ReadonlyConfig.fromMap(new HashMap<>());
+ Map<String, String> tableOptions =
Collections.singletonMap("replication_num", "3");
+
+ Assertions.assertDoesNotThrow(
+ () ->
StarRocksSaveModeUtil.INSTANCE.validateTableOptions(config, tableOptions));
+ }
+
+ @Test
+ void validateRejectsCustomTemplateWithTableOptions() {
+ Map<String, Object> configMap = new HashMap<>();
+ configMap.put(
+ StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key(),
+ "CREATE TABLE `${database}`.`${table}` (${rowtype_fields})");
+ ReadonlyConfig config = ReadonlyConfig.fromMap(configMap);
+ Map<String, String> tableOptions =
Collections.singletonMap("replication_num", "3");
+
+ SeaTunnelRuntimeException exception =
+ Assertions.assertThrows(
+ SeaTunnelRuntimeException.class,
+ () ->
+
StarRocksSaveModeUtil.INSTANCE.validateTableOptions(
+ config, tableOptions));
+
+ Assertions.assertTrue(exception.getMessage().contains("custom
save_mode_create_template"));
+ }
+
+ @Test
+ void applyMergesPropertiesIntoSql() {
+ String sql =
+ "CREATE TABLE t (id INT) ENGINE=OLAP PROPERTIES (\n"
+ + " \"replication_num\" = \"1\"\n"
+ + ")";
+ Map<String, String> tableOptions =
Collections.singletonMap("replication_num", "3");
+
+ String merged =
+
StarRocksSaveModeUtil.INSTANCE.applyTableOptionsToCreateTableSql(sql,
tableOptions);
+
+ Assertions.assertTrue(merged.contains("\"replication_num\" = \"3\""));
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsConditionExtensionTest.java
b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsConditionExtensionTest.java
new file mode 100644
index 0000000000..47cade3904
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-starrocks/src/test/java/org/apache/seatunnel/connectors/seatunnel/starrocks/sink/StarRocksTableOptionsConditionExtensionTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.seatunnel.connectors.seatunnel.starrocks.sink;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConfigValidator;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
+import org.apache.seatunnel.api.options.SinkConnectorCommonOptions;
+import
org.apache.seatunnel.connectors.seatunnel.starrocks.config.StarRocksSinkOptions;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+class StarRocksTableOptionsConditionExtensionTest {
+
+ @Test
+ void testDefaultTemplateWithTableOptionsPassViaOptionRule() {
+ Map<String, Object> config = starRocksSinkConfig();
+ Map<String, String> tableOptions = new HashMap<>();
+ tableOptions.put("replication_num", "3");
+ config.put(SinkConnectorCommonOptions.TABLE_OPTIONS.key(),
tableOptions);
+
+ Assertions.assertDoesNotThrow(() -> validateSinkOptionRule(config));
+ }
+
+ @Test
+ void testCustomTemplateRejectsTableOptionsViaOptionRule() {
+ Map<String, Object> config = starRocksSinkConfig();
+ config.put(
+ StarRocksSinkOptions.SAVE_MODE_CREATE_TEMPLATE.key(),
+ "CREATE TABLE `${database}`.`${table}` (${rowtype_fields})");
+ Map<String, String> tableOptions = new HashMap<>();
+ tableOptions.put("replication_num", "3");
+ config.put(SinkConnectorCommonOptions.TABLE_OPTIONS.key(),
tableOptions);
+
+ OptionValidationException exception =
+ Assertions.assertThrows(
+ OptionValidationException.class, () ->
validateSinkOptionRule(config));
+ Assertions.assertTrue(exception.getMessage().contains("custom
save_mode_create_template"));
+ }
+
+ @Test
+ void testAbsentTableOptionsSkipsExtension() {
+ Assertions.assertDoesNotThrow(() ->
validateSinkOptionRule(starRocksSinkConfig()));
+ }
+
+ @Test
+ void testEmptyTableOptionsSkipsExtension() {
+ Map<String, Object> config = starRocksSinkConfig();
+ config.put(SinkConnectorCommonOptions.TABLE_OPTIONS.key(), new
HashMap<>());
+
+ Assertions.assertDoesNotThrow(() -> validateSinkOptionRule(config));
+ }
+
+ private static void validateSinkOptionRule(Map<String, Object> config) {
+ ConfigValidator.of(ReadonlyConfig.fromMap(config))
+ .validate(new StarRocksSinkFactory().optionRule());
+ }
+
+ private static Map<String, Object> starRocksSinkConfig() {
+ Map<String, Object> config = new HashMap<>();
+ config.put("base-url", "jdbc:mysql://127.0.0.1:9030");
+ config.put("nodeUrls", Arrays.asList("127.0.0.1:8030"));
+ config.put("username", "root");
+ config.put("password", "");
+ config.put("database", "test");
+ return config;
+ }
+}
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-starrocks-e2e/src/test/java/org/apache/seatunnel/e2e/connector/starrocks/StarRocksIT.java
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-starrocks-e2e/src/test/java/org/apache/seatunnel/e2e/connector/starrocks/StarRocksIT.java
index 872ce538c4..1af0d27988 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-starrocks-e2e/src/test/java/org/apache/seatunnel/e2e/connector/starrocks/StarRocksIT.java
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-starrocks-e2e/src/test/java/org/apache/seatunnel/e2e/connector/starrocks/StarRocksIT.java
@@ -78,6 +78,9 @@ public class StarRocksIT extends TestSuiteBase implements
TestResource {
private static final String SOURCE_TABLE = "e2e_table_source";
private static final String SOURCE_TABLE_3 = "e2e_table_source_3";
private static final String SINK_TABLE = "e2e_table_sink";
+ private static final String TABLE_OPTIONS_SINK_TABLE =
"sink_table_options";
+ private static final String TABLE_OPTIONS_CONFIG_FILE =
+ "/fake-to-starrocks-with-table-options.conf";
private static final String SR_DRIVER_JAR =
"https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.16/mysql-connector-java-8.0.16.jar";
private static final String COLUMN_STRING =
@@ -342,6 +345,19 @@ public class StarRocksIT extends TestSuiteBase implements
TestResource {
Assertions.assertEquals(0, execResult.getExitCode(),
execResult.getStderr());
}
+ @TestTemplate
+ public void testTableOptionsSink(TestContainer container)
+ throws IOException, InterruptedException, SQLException {
+ try {
+ dropTableIfExists(TABLE_OPTIONS_SINK_TABLE);
+ Container.ExecResult execResult =
container.executeJob(TABLE_OPTIONS_CONFIG_FILE);
+ Assertions.assertEquals(0, execResult.getExitCode(),
execResult.getStderr());
+ assertTableOptionsSink();
+ } finally {
+ dropTableIfExists(TABLE_OPTIONS_SINK_TABLE);
+ }
+ }
+
private void initializeJdbcConnection()
throws SQLException, ClassNotFoundException, MalformedURLException,
InstantiationException, IllegalAccessException {
@@ -409,6 +425,35 @@ public class StarRocksIT extends TestSuiteBase implements
TestResource {
}
}
+ private void assertTableOptionsSink() throws SQLException {
+ try (Statement statement = jdbcConnection.createStatement()) {
+ ResultSet createTableResult =
+ statement.executeQuery(
+ String.format(
+ "SHOW CREATE TABLE %s.%s", DATABASE,
TABLE_OPTIONS_SINK_TABLE));
+ Assertions.assertTrue(createTableResult.next());
+ String createTableSql =
createTableResult.getString(2).toLowerCase();
+ Assertions.assertTrue(
+ createTableSql.contains("\"storage_format\" = \"v2\""),
createTableSql);
+
+ ResultSet countResult =
+ statement.executeQuery(
+ String.format(
+ "SELECT COUNT(*) FROM %s.%s",
+ DATABASE, TABLE_OPTIONS_SINK_TABLE));
+ Assertions.assertTrue(countResult.next());
+ Assertions.assertEquals(100, countResult.getInt(1));
+ }
+ }
+
+ private void dropTableIfExists(String table) {
+ try (Statement statement = jdbcConnection.createStatement()) {
+ statement.execute(String.format("DROP TABLE IF EXISTS %s.%s",
DATABASE, table));
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed to drop table " + table, e);
+ }
+ }
+
@Test
public void testCatalog() {
TablePath tablePathStarRocksSource = TablePath.of("test",
"e2e_table_source");
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-starrocks-e2e/src/test/resources/fake-to-starrocks-with-table-options.conf
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-starrocks-e2e/src/test/resources/fake-to-starrocks-with-table-options.conf
new file mode 100644
index 0000000000..107fac6770
--- /dev/null
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-starrocks-e2e/src/test/resources/fake-to-starrocks-with-table-options.conf
@@ -0,0 +1,74 @@
+#
+# 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.
+#
+
+env {
+ parallelism = 1
+ job.mode = "BATCH"
+}
+
+source {
+ FakeSource {
+ parallelism = 1
+ plugin_output = "fake"
+ row.num = 100
+ schema {
+ table = "FakeTable"
+ primaryKey {
+ name = "pk_id"
+ columnNames = [id]
+ }
+ columns = [
+ {
+ name = id
+ type = bigint
+ nullable = false
+ },
+ {
+ name = c_string
+ type = string
+ nullable = true
+ }
+ ]
+ }
+ }
+}
+
+transform {
+}
+
+sink {
+ StarRocks {
+ plugin_input = "fake"
+ nodeUrls = ["starrocks_e2e:8030"]
+ username = root
+ password = ""
+ database = "test"
+ table = "sink_table_options"
+ batch_max_rows = 100
+ max_retries = 3
+ base-url="jdbc:mysql://starrocks_e2e:9030/test"
+ starrocks.config = {
+ format = "JSON"
+ strip_outer_array = true
+ }
+ schema_save_mode = "CREATE_SCHEMA_WHEN_NOT_EXIST"
+ data_save_mode = "APPEND_DATA"
+ table_options = {
+ storage_format = "V2"
+ }
+ }
+}