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 2714e6e25c [Improve][Connector-V2][Doris] Support partition cleanup
for DROP_DATA (#11917)
2714e6e25c is described below
commit 2714e6e25c67adb0f26eff7327368749041d1e5b
Author: Goutam Adwant <[email protected]>
AuthorDate: Sun Aug 23 06:27:04 2026 -0700
[Improve][Connector-V2][Doris] Support partition cleanup for DROP_DATA
(#11917)
Signed-off-by: goutamadwant <[email protected]>
---
docs/en/connectors/sink/Doris.md | 4 +-
docs/zh/connectors/sink/Doris.md | 4 +-
.../connectors/doris/catalog/DorisCatalog.java | 14 ++-
.../connectors/doris/config/DorisSinkConfig.java | 49 ++++++++
.../seatunnel/connectors/doris/sink/DorisSink.java | 13 ++-
.../doris/sink/savemode/DorisSaveModeHandler.java | 59 ++++++++++
.../connectors/doris/util/DorisCatalogUtil.java | 19 +++
.../doris/config/DorisSinkConfigTest.java | 66 +++++++++++
.../sink/savemode/DorisSaveModeHandlerTest.java | 128 +++++++++++++++++++++
.../doris/util/DorisCatalogUtilTest.java | 32 ++++++
10 files changed, 378 insertions(+), 10 deletions(-)
diff --git a/docs/en/connectors/sink/Doris.md b/docs/en/connectors/sink/Doris.md
index 5afacfc0bd..9db57c496d 100644
--- a/docs/en/connectors/sink/Doris.md
+++ b/docs/en/connectors/sink/Doris.md
@@ -65,7 +65,7 @@ The internal implementation of Doris sink connector is cached
and imported by st
| data_save_mode | Enum | no | APPEND_DATA
| the data save mode, please refer to `data_save_mode` below
|
| save_mode_create_template | string | no | see below
| 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.
|
-| doris.config | map | yes | -
| Stream Load data description parameters passed to Doris. The most
common keys are `format` (`json` or `csv`), `read_json_by_line`
(`true`/`false`), `column_separator`, and `row_delimiter`. See the Doris Stream
Load documentation for the full key set. |
+| doris.config | map | yes | -
| Stream Load data description parameters passed to Doris. The most
common keys are `format` (`json` or `csv`), `read_json_by_line`
(`true`/`false`), `column_separator`, `row_delimiter`, and `partitions`. See
the Doris Stream Load documentation for the full key set. |
## Redirect Behavior
@@ -93,7 +93,7 @@ Option introduction:
Before the synchronous task is turned on, different processing schemes are
selected for data existing data on the target side.
Option introduction:
-`DROP_DATA`: Preserve database structure and delete data
+`DROP_DATA`: Preserve database structure and delete data. By default the
complete target table is truncated. When `doris.config.partitions` contains a
comma-separated list of formal Doris partition names, only those partitions are
truncated and subsequent Stream Load writes are restricted to the same
partitions. Every target in a multi-table job must contain the configured
partition names. Doris rejects cleanup when a partition does not exist.
Partition values and temporary partitions ar [...]
`APPEND_DATA`:Preserve database structure, preserve data
`CUSTOM_PROCESSING`:User defined processing
`ERROR_WHEN_DATA_EXISTS`:When there is data, an error is reported
diff --git a/docs/zh/connectors/sink/Doris.md b/docs/zh/connectors/sink/Doris.md
index 70af62651b..627f2fe66f 100644
--- a/docs/zh/connectors/sink/Doris.md
+++ b/docs/zh/connectors/sink/Doris.md
@@ -65,7 +65,7 @@ Doris Sink连接器的内部实现是通过stream load批量缓存和导入的
| data_save_mode | Enum | no | APPEND_DATA
| 数据保存模式,请参考下面的`data_save_mode`。
|
| save_mode_create_template | string | no | see below
| 见下文。
|
| custom_sql | String | no | -
| 当data_save_mode选择CUSTOM_PROCESSING时,需要填写CUSTOM_SQL参数。
该参数通常填写一条可以执行的SQL。 SQL将在同步任务之前执行。
|
-| doris.config | map | yes | -
| 该选项用于支持自动生成sql时的insert、delete、update等操作,以及支持的格式。
|
+| doris.config | map | yes | -
| 传递给 Doris Stream Load 的数据描述参数。常用参数包括
`format`、`read_json_by_line`、`column_separator`、`row_delimiter` 和 `partitions`。
|
## Redirect 行为说明
@@ -93,7 +93,7 @@ Doris Sink连接器的内部实现是通过stream load批量缓存和导入的
在开启同步任务之前,针对目标端已有的数据选择不同的处理方案。
选项介绍:
-`DROP_DATA`: 保留数据库结构并删除数据。
+`DROP_DATA`: 保留数据库结构并删除数据。默认清空整个目标表。当 `doris.config.partitions` 配置了以逗号分隔的
Doris 正式分区名称时,只清空这些分区,后续 Stream Load 也只写入相同分区。多表任务中的每个目标表都必须包含这些分区;分区不存在时 Doris
会拒绝清理。不支持分区值或临时分区。
`APPEND_DATA`:保留数据库结构,保留数据。
`CUSTOM_PROCESSING`:用户自定义处理。
`ERROR_WHEN_DATA_EXISTS`:有数据时报错。
diff --git
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalog.java
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalog.java
index 20e25e6cc6..f06f382bcf 100644
---
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalog.java
+++
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/catalog/DorisCatalog.java
@@ -471,9 +471,21 @@ public class DorisCatalog implements Catalog {
public void truncateTable(TablePath tablePath, boolean ignoreIfNotExists)
throws TableNotExistException, CatalogException {
+ truncateTable(tablePath, ignoreIfNotExists, Collections.emptyList());
+ }
+
+ /**
+ * Truncates the requested partitions, or the complete table when no
partitions are supplied.
+ */
+ public void truncateTable(
+ TablePath tablePath, boolean ignoreIfNotExists, List<String>
partitions)
+ throws TableNotExistException, CatalogException {
try {
if (ignoreIfNotExists) {
-
conn.createStatement().execute(DorisCatalogUtil.getTruncateTableQuery(tablePath));
+ try (Statement statement = conn.createStatement()) {
+ statement.execute(
+ DorisCatalogUtil.getTruncateTableQuery(tablePath,
partitions));
+ }
}
} catch (Exception e) {
throw new CatalogException(
diff --git
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfig.java
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfig.java
index 26294865d3..7bfde8acfe 100644
---
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfig.java
+++
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfig.java
@@ -20,7 +20,10 @@ package org.apache.seatunnel.connectors.doris.config;
import org.apache.seatunnel.shade.com.typesafe.config.Config;
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.sink.DataSaveMode;
+import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException;
import lombok.Getter;
import lombok.Setter;
@@ -28,8 +31,13 @@ import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Properties;
+import java.util.Set;
import static
org.apache.seatunnel.connectors.doris.config.DorisBaseOptions.DATABASE;
import static
org.apache.seatunnel.connectors.doris.config.DorisBaseOptions.DORIS_BATCH_SIZE;
@@ -40,6 +48,7 @@ import static
org.apache.seatunnel.connectors.doris.config.DorisBaseOptions.TABL
import static
org.apache.seatunnel.connectors.doris.config.DorisBaseOptions.USERNAME;
import static
org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.BENODES;
import static
org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.CASE_SENSITIVE;
+import static
org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.DATA_SAVE_MODE;
import static
org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.DIRECT_TO_BE;
import static
org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.DORIS_SINK_CONFIG_PREFIX;
import static
org.apache.seatunnel.connectors.doris.config.DorisSinkOptions.NEEDS_UNSUPPORTED_TYPE_CASTING;
@@ -77,6 +86,7 @@ public class DorisSinkConfig implements Serializable {
private Integer bufferSize;
private Integer bufferCount;
private Properties streamLoadProps;
+ private List<String> partitions = Collections.emptyList();
private boolean directToBe;
private boolean needsUnsupportedTypeCasting;
private boolean caseSensitive;
@@ -99,6 +109,15 @@ public class DorisSinkConfig implements Serializable {
dorisSinkConfig.setPassword(config.get(PASSWORD));
dorisSinkConfig.setQueryPort(config.get(QUERY_PORT));
dorisSinkConfig.setStreamLoadProps(parseStreamLoadProperties(config));
+ if (config.get(DATA_SAVE_MODE) == DataSaveMode.DROP_DATA) {
+ List<String> partitions =
parseDropDataPartitions(dorisSinkConfig.getStreamLoadProps());
+ dorisSinkConfig.setPartitions(partitions);
+ if (!partitions.isEmpty()) {
+ dorisSinkConfig
+ .getStreamLoadProps()
+ .setProperty("partitions", String.join(",",
partitions));
+ }
+ }
dorisSinkConfig.setDatabase(config.get(DATABASE));
dorisSinkConfig.setTable(config.get(TABLE));
dorisSinkConfig.setBatchSize(config.get(DORIS_BATCH_SIZE));
@@ -136,4 +155,34 @@ public class DorisSinkConfig implements Serializable {
}
return streamLoadProps;
}
+ /**
+ * Parses a comma-separated partition list for partition-scoped DROP_DATA
cleanup.
+ *
+ * @throws DorisConnectorException if a partition name is blank or
duplicated
+ */
+ private static List<String> parseDropDataPartitions(Properties
streamLoadProperties) {
+ String configuredPartitions =
streamLoadProperties.getProperty("partitions");
+ if (configuredPartitions == null) {
+ return Collections.emptyList();
+ }
+
+ String[] values = configuredPartitions.split(",", -1);
+ List<String> partitions = new ArrayList<>(values.length);
+ Set<String> uniquePartitions = new HashSet<>();
+ for (String value : values) {
+ String partition = value.trim();
+ if (partition.isEmpty()) {
+ throw new DorisConnectorException(
+ SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
+ "PluginName: Doris, Message: 'doris.config.partitions'
cannot contain blank partition names.");
+ }
+ if (!uniquePartitions.add(partition)) {
+ throw new DorisConnectorException(
+ SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
+ "PluginName: Doris, Message: 'doris.config.partitions'
cannot contain duplicate partition names.");
+ }
+ partitions.add(partition);
+ }
+ return Collections.unmodifiableList(partitions);
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSink.java
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSink.java
index 88bee59012..b592d38972 100644
---
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSink.java
+++
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/DorisSink.java
@@ -21,7 +21,6 @@ import org.apache.seatunnel.api.common.JobContext;
import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode;
import org.apache.seatunnel.api.configuration.ReadonlyConfig;
import org.apache.seatunnel.api.serialization.Serializer;
-import org.apache.seatunnel.api.sink.DefaultSaveModeHandler;
import org.apache.seatunnel.api.sink.SaveModeHandler;
import org.apache.seatunnel.api.sink.SeaTunnelSink;
import org.apache.seatunnel.api.sink.SinkCommitter;
@@ -29,18 +28,19 @@ import org.apache.seatunnel.api.sink.SinkWriter;
import org.apache.seatunnel.api.sink.SupportMultiTableSink;
import org.apache.seatunnel.api.sink.SupportSaveMode;
import org.apache.seatunnel.api.sink.SupportSchemaEvolutionSink;
-import org.apache.seatunnel.api.table.catalog.Catalog;
import org.apache.seatunnel.api.table.catalog.CatalogTable;
import org.apache.seatunnel.api.table.factory.CatalogFactory;
import org.apache.seatunnel.api.table.schema.SchemaChangeType;
import org.apache.seatunnel.api.table.type.SeaTunnelRow;
import org.apache.seatunnel.common.constants.PluginType;
+import org.apache.seatunnel.connectors.doris.catalog.DorisCatalog;
import org.apache.seatunnel.connectors.doris.config.DorisSinkConfig;
import org.apache.seatunnel.connectors.doris.config.DorisSinkOptions;
import org.apache.seatunnel.connectors.doris.exception.DorisConnectorException;
import org.apache.seatunnel.connectors.doris.sink.committer.DorisCommitInfo;
import
org.apache.seatunnel.connectors.doris.sink.committer.DorisCommitInfoSerializer;
import org.apache.seatunnel.connectors.doris.sink.committer.DorisCommitter;
+import
org.apache.seatunnel.connectors.doris.sink.savemode.DorisSaveModeHandler;
import org.apache.seatunnel.connectors.doris.sink.writer.DorisSinkState;
import
org.apache.seatunnel.connectors.doris.sink.writer.DorisSinkStateSerializer;
import org.apache.seatunnel.connectors.doris.sink.writer.DorisSinkWriter;
@@ -155,14 +155,17 @@ public class DorisSink
getPluginName(), PluginType.SINK, "Cannot find
Doris catalog factory"));
}
- Catalog catalog =
catalogFactory.createCatalog(catalogFactory.factoryIdentifier(), config);
+ DorisCatalog catalog =
+ (DorisCatalog)
+
catalogFactory.createCatalog(catalogFactory.factoryIdentifier(), config);
return Optional.of(
- new DefaultSaveModeHandler(
+ new DorisSaveModeHandler(
config.get(DorisSinkOptions.SCHEMA_SAVE_MODE),
config.get(DorisSinkOptions.DATA_SAVE_MODE),
catalog,
catalogTable,
- config.get(DorisSinkOptions.CUSTOM_SQL)));
+ config.get(DorisSinkOptions.CUSTOM_SQL),
+ dorisSinkConfig.getPartitions()));
}
@Override
diff --git
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/savemode/DorisSaveModeHandler.java
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/savemode/DorisSaveModeHandler.java
new file mode 100644
index 0000000000..093c70de20
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/sink/savemode/DorisSaveModeHandler.java
@@ -0,0 +1,59 @@
+/*
+ * 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.doris.sink.savemode;
+
+import org.apache.seatunnel.api.sink.DataSaveMode;
+import org.apache.seatunnel.api.sink.DefaultSaveModeHandler;
+import org.apache.seatunnel.api.sink.SchemaSaveMode;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.connectors.doris.catalog.DorisCatalog;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+/** Applies Doris-specific partition cleanup for the DROP_DATA save mode. */
+@Slf4j
+public class DorisSaveModeHandler extends DefaultSaveModeHandler {
+
+ private final List<String> partitions;
+
+ private final DorisCatalog dorisCatalog;
+
+ public DorisSaveModeHandler(
+ SchemaSaveMode schemaSaveMode,
+ DataSaveMode dataSaveMode,
+ DorisCatalog catalog,
+ CatalogTable catalogTable,
+ String customSql,
+ List<String> partitions) {
+ super(schemaSaveMode, dataSaveMode, catalog, catalogTable, customSql);
+ this.dorisCatalog = catalog;
+ this.partitions = partitions;
+ }
+
+ @Override
+ protected void truncateTable() {
+ if (partitions.isEmpty()) {
+ super.truncateTable();
+ return;
+ }
+ log.info("Truncating partitions {} in table {}", partitions,
tablePath);
+ dorisCatalog.truncateTable(tablePath, true, partitions);
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java
index 0a169a6d74..2ca95a0dd3 100644
---
a/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java
+++
b/seatunnel-connectors-v2/connector-doris/src/main/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtil.java
@@ -102,6 +102,25 @@ public class DorisCatalogUtil {
return "TRUNCATE TABLE " + tablePath.getFullName();
}
+ /** Builds a table-wide or partition-scoped Doris TRUNCATE TABLE
statement. */
+ public static String getTruncateTableQuery(TablePath tablePath,
List<String> partitions) {
+ String query = getTruncateTableQuery(tablePath);
+ if (partitions == null || partitions.isEmpty()) {
+ return query;
+ }
+ return query
+ + " PARTITION ("
+ + partitions.stream()
+ .map(DorisCatalogUtil::quoteIdentifier)
+ .collect(Collectors.joining(", "))
+ + ")";
+ }
+
+ /** Quotes a Doris identifier and escapes embedded backticks. */
+ private static String quoteIdentifier(String identifier) {
+ return "`" + identifier.replace("`", "``") + "`";
+ }
+
/**
* @param createTableTemplate create table template
* @param catalogTable catalog table
diff --git
a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfigTest.java
b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfigTest.java
index 7024bb5070..bb6cc34109 100644
---
a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfigTest.java
+++
b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/config/DorisSinkConfigTest.java
@@ -20,16 +20,73 @@ package org.apache.seatunnel.connectors.doris.config;
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.connectors.doris.exception.DorisConnectorException;
import org.apache.seatunnel.connectors.doris.sink.DorisSinkFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class DorisSinkConfigTest {
+ @Test
+ public void testDropDataPartitionsAreNormalized() {
+ Map<String, Object> options = new HashMap<>();
+ options.put("data_save_mode", "DROP_DATA");
+ options.put("doris.config", Collections.singletonMap("partitions", "
p1 , p2 "));
+
+ DorisSinkConfig sinkConfig = DorisSinkConfig.of(createConfig(options));
+
+ Assertions.assertEquals(Arrays.asList("p1", "p2"),
sinkConfig.getPartitions());
+ Assertions.assertEquals("p1,p2",
sinkConfig.getStreamLoadProps().getProperty("partitions"));
+ }
+
+ @Test
+ public void testDropDataRejectsBlankPartitionNames() {
+ Map<String, Object> options = new HashMap<>();
+ options.put("data_save_mode", "DROP_DATA");
+ options.put("doris.config", Collections.singletonMap("partitions",
"p1,,p2"));
+ ReadonlyConfig config = createConfig(options);
+
+ DorisConnectorException exception =
+ Assertions.assertThrows(
+ DorisConnectorException.class, () ->
DorisSinkConfig.of(config));
+
+ Assertions.assertTrue(exception.getMessage().contains("blank partition
names"));
+ }
+
+ @Test
+ public void testDropDataRejectsDuplicatePartitionNames() {
+ Map<String, Object> options = new HashMap<>();
+ options.put("data_save_mode", "DROP_DATA");
+ options.put("doris.config", Collections.singletonMap("partitions",
"p1,p1"));
+ ReadonlyConfig config = createConfig(options);
+
+ DorisConnectorException exception =
+ Assertions.assertThrows(
+ DorisConnectorException.class, () ->
DorisSinkConfig.of(config));
+
+ Assertions.assertTrue(exception.getMessage().contains("duplicate
partition names"));
+ }
+
+ @Test
+ public void testAppendDataDoesNotInterpretStreamLoadPartitions() {
+ DorisSinkConfig sinkConfig =
+ DorisSinkConfig.of(
+ createConfig(
+ Collections.singletonMap(
+ "doris.config",
+ Collections.singletonMap("partitions",
"p1,,p2"))));
+
+ Assertions.assertTrue(sinkConfig.getPartitions().isEmpty());
+ Assertions.assertEquals(
+ "p1,,p2",
sinkConfig.getStreamLoadProps().getProperty("partitions"));
+ }
+
@Test
public void testDirectToBeRequiresBenodes() {
Map<String, Object> configMap = baseConfig();
@@ -78,4 +135,13 @@ public class DorisSinkConfigTest {
config.put("doris.config", new HashMap<String, String>());
return config;
}
+
+ private static ReadonlyConfig createConfig(Map<String, Object>
extraOptions) {
+ Map<String, Object> config = baseConfig();
+ config.put("database", "test_db");
+ config.put("table", "test_table");
+ config.put("sink.label-prefix", "test_job");
+ config.putAll(extraOptions);
+ return ReadonlyConfig.fromMap(config);
+ }
}
diff --git
a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/sink/savemode/DorisSaveModeHandlerTest.java
b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/sink/savemode/DorisSaveModeHandlerTest.java
new file mode 100644
index 0000000000..ea295ae363
--- /dev/null
+++
b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/sink/savemode/DorisSaveModeHandlerTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.doris.sink.savemode;
+
+import org.apache.seatunnel.api.sink.DataSaveMode;
+import org.apache.seatunnel.api.sink.SchemaSaveMode;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.TableIdentifier;
+import org.apache.seatunnel.api.table.catalog.TablePath;
+import org.apache.seatunnel.connectors.doris.catalog.DorisCatalog;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class DorisSaveModeHandlerTest {
+
+ private static final TableIdentifier TABLE_IDENTIFIER =
+ TableIdentifier.of("doris", "test_db", "test_table");
+ private static final TablePath TABLE_PATH = TABLE_IDENTIFIER.toTablePath();
+
+ @Test
+ void dropDataTruncatesConfiguredPartitions() {
+ DorisCatalog catalog = mock(DorisCatalog.class);
+ CatalogTable catalogTable = mock(CatalogTable.class);
+ when(catalogTable.getTableId()).thenReturn(TABLE_IDENTIFIER);
+ when(catalog.tableExists(TABLE_PATH)).thenReturn(true);
+ DorisSaveModeHandler handler =
+ new DorisSaveModeHandler(
+ SchemaSaveMode.IGNORE,
+ DataSaveMode.DROP_DATA,
+ catalog,
+ catalogTable,
+ null,
+ Arrays.asList("p1", "p2"));
+
+ handler.handleDataSaveMode();
+
+ verify(catalog).truncateTable(TABLE_PATH, true, Arrays.asList("p1",
"p2"));
+ verify(catalog, never()).truncateTable(TABLE_PATH, true);
+ }
+
+ @Test
+ void dropDataWithoutPartitionsPreservesWholeTableCleanup() {
+ DorisCatalog catalog = mock(DorisCatalog.class);
+ CatalogTable catalogTable = mock(CatalogTable.class);
+ when(catalogTable.getTableId()).thenReturn(TABLE_IDENTIFIER);
+ when(catalog.tableExists(TABLE_PATH)).thenReturn(true);
+ DorisSaveModeHandler handler =
+ new DorisSaveModeHandler(
+ SchemaSaveMode.IGNORE,
+ DataSaveMode.DROP_DATA,
+ catalog,
+ catalogTable,
+ null,
+ Collections.emptyList());
+
+ handler.handleDataSaveMode();
+
+ verify(catalog).truncateTable(TABLE_PATH, true);
+ verify(catalog, never()).truncateTable(TABLE_PATH, true,
Collections.emptyList());
+ }
+
+ @Test
+ void appendDataDoesNotTruncatePartitions() {
+ DorisCatalog catalog = mock(DorisCatalog.class);
+ CatalogTable catalogTable = mock(CatalogTable.class);
+ when(catalogTable.getTableId()).thenReturn(TABLE_IDENTIFIER);
+ DorisSaveModeHandler handler =
+ new DorisSaveModeHandler(
+ SchemaSaveMode.IGNORE,
+ DataSaveMode.APPEND_DATA,
+ catalog,
+ catalogTable,
+ null,
+ Collections.singletonList("p1"));
+
+ handler.handleDataSaveMode();
+
+ verify(catalog, never()).truncateTable(TABLE_PATH, true);
+ verify(catalog, never()).truncateTable(TABLE_PATH, true,
Collections.singletonList("p1"));
+ }
+
+ @Test
+ void dropDataDoesNotTruncateNewlyCreatedTable() {
+ DorisCatalog catalog = mock(DorisCatalog.class);
+ CatalogTable catalogTable = mock(CatalogTable.class);
+ when(catalogTable.getTableId()).thenReturn(TABLE_IDENTIFIER);
+
when(catalog.databaseExists(TABLE_PATH.getDatabaseName())).thenReturn(true);
+ when(catalog.tableExists(TABLE_PATH)).thenReturn(false, true);
+ DorisSaveModeHandler handler =
+ new DorisSaveModeHandler(
+ SchemaSaveMode.CREATE_SCHEMA_WHEN_NOT_EXIST,
+ DataSaveMode.DROP_DATA,
+ catalog,
+ catalogTable,
+ null,
+ Collections.singletonList("p1"));
+
+ handler.handleSchemaSaveMode();
+ handler.handleDataSaveMode();
+
+ verify(catalog).createTable(TABLE_PATH, catalogTable, true);
+ verify(catalog, never()).truncateTable(TABLE_PATH, true);
+ verify(catalog, never()).truncateTable(TABLE_PATH, true,
Collections.singletonList("p1"));
+ }
+}
diff --git
a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtilTest.java
b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtilTest.java
index 4228a943cc..d28b7fae44 100644
---
a/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtilTest.java
+++
b/seatunnel-connectors-v2/connector-doris/src/test/java/org/apache/seatunnel/connectors/doris/util/DorisCatalogUtilTest.java
@@ -19,6 +19,7 @@ package org.apache.seatunnel.connectors.doris.util;
import org.apache.seatunnel.api.table.catalog.Column;
import org.apache.seatunnel.api.table.catalog.PhysicalColumn;
+import org.apache.seatunnel.api.table.catalog.TablePath;
import org.apache.seatunnel.api.table.converter.BasicTypeDefine;
import org.apache.seatunnel.api.table.converter.TypeConverter;
import org.apache.seatunnel.api.table.type.BasicType;
@@ -28,12 +29,43 @@ import
org.apache.seatunnel.connectors.doris.datatype.DorisTypeConverterV2;
import org.junit.jupiter.api.Test;
+import java.util.Arrays;
+import java.util.Collections;
+
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class DorisCatalogUtilTest {
+ @Test
+ void buildsWholeTableTruncateQueryWithoutPartitions() {
+ TablePath tablePath = TablePath.of("test_db", "test_table");
+
+ assertEquals(
+ "TRUNCATE TABLE test_db.test_table",
+ DorisCatalogUtil.getTruncateTableQuery(tablePath,
Collections.emptyList()));
+ }
+
+ @Test
+ void buildsPartitionTruncateQueryWithQuotedIdentifiers() {
+ TablePath tablePath = TablePath.of("test_db", "test_table");
+
+ assertEquals(
+ "TRUNCATE TABLE test_db.test_table PARTITION (`p1`, `p``2`)",
+ DorisCatalogUtil.getTruncateTableQuery(tablePath,
Arrays.asList("p1", "p`2")));
+ }
+
+ @Test
+ void keepsPartitionContentInsideQuotedIdentifier() {
+ TablePath tablePath = TablePath.of("test_db", "test_table");
+
+ assertEquals(
+ "TRUNCATE TABLE test_db.test_table PARTITION (`p1``) DROP
TABLE test_db.other; --`)",
+ DorisCatalogUtil.getTruncateTableQuery(
+ tablePath, Collections.singletonList("p1`) DROP TABLE
test_db.other; --")));
+ }
+
@Test
void returnsReconvertedTypeWhenSinkTypeNotNull() {
Column column = mock(Column.class);