This is an automated email from the ASF dual-hosted git repository.
nzw921rx 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 db52cb7b03 [Improve][Connector-V2] Validate nonblank Lance sink
options (#12222)
db52cb7b03 is described below
commit db52cb7b03e1b44e6e9df1ac7c06751f894f6cf8
Author: ZYZ666-RGB <[email protected]>
AuthorDate: Wed Sep 9 21:33:45 2026 -0700
[Improve][Connector-V2] Validate nonblank Lance sink options (#12222)
---
docs/en/connectors/sink/Lance.md | 4 +
docs/zh/connectors/sink/Lance.md | 4 +
.../seatunnel/lance/sink/LanceSinkFactory.java | 7 +-
.../seatunnel/lance/LanceFactoryTest.java | 111 ++++++++++++++++++++-
4 files changed, 124 insertions(+), 2 deletions(-)
diff --git a/docs/en/connectors/sink/Lance.md b/docs/en/connectors/sink/Lance.md
index 113240fc4a..e60f872ff9 100644
--- a/docs/en/connectors/sink/Lance.md
+++ b/docs/en/connectors/sink/Lance.md
@@ -64,10 +64,14 @@ It provides a sink only; there is no Lance source connector.
The directory or dataset path where Lance data is stored. In local directory
mode, make sure the SeaTunnel runtime can create and write to this path.
+If specified, this value must not be empty or contain only whitespace.
Omitting it keeps the default `/test.lance`.
+
### namespace_type
The Lance namespace type. Currently the connector supports `dir`.
+If specified, this value must not be empty or contain only whitespace.
Omitting it keeps the default `dir`.
+
### namespace_id
The namespace name used by the directory namespace implementation. In local
diff --git a/docs/zh/connectors/sink/Lance.md b/docs/zh/connectors/sink/Lance.md
index 59c478fbb8..95636f1d6c 100644
--- a/docs/zh/connectors/sink/Lance.md
+++ b/docs/zh/connectors/sink/Lance.md
@@ -62,10 +62,14 @@ Lance sink 用于把 SeaTunnel 数据写入 Lance 数据集。它可以根据上
Lance 数据的目录或数据集路径。使用本地目录模式时,请确保 SeaTunnel 运行环境有权限创建并写入该路径。
+显式配置时,该值不能为空字符串或仅包含空白字符。省略时仍使用默认值 `/test.lance`。
+
### namespace_type
Lance namespace 类型。当前连接器支持 `dir`。
+显式配置时,该值不能为空字符串或仅包含空白字符。省略时仍使用默认值 `dir`。
+
### namespace_id
目录 namespace 实现使用的 namespace 名称。本地目录模式下可以填写类似 `root` 的简单名称。
diff --git
a/seatunnel-connectors-v2/connector-lance/src/main/java/org/apache/seatunnel/connectors/seatunnel/lance/sink/LanceSinkFactory.java
b/seatunnel-connectors-v2/connector-lance/src/main/java/org/apache/seatunnel/connectors/seatunnel/lance/sink/LanceSinkFactory.java
index 007aeb1b0d..699f802234 100644
---
a/seatunnel-connectors-v2/connector-lance/src/main/java/org/apache/seatunnel/connectors/seatunnel/lance/sink/LanceSinkFactory.java
+++
b/seatunnel-connectors-v2/connector-lance/src/main/java/org/apache/seatunnel/connectors/seatunnel/lance/sink/LanceSinkFactory.java
@@ -36,6 +36,8 @@ import org.apache.commons.collections4.CollectionUtils;
import com.google.auto.service.AutoService;
+import static org.apache.seatunnel.api.configuration.util.Conditions.notBlank;
+
@AutoService(Factory.class)
public class LanceSinkFactory implements TableSinkFactory {
@Override
@@ -55,7 +57,10 @@ public class LanceSinkFactory implements TableSinkFactory {
public OptionRule optionRule() {
return OptionRule.builder()
.required(
- LanceCommonOptions.KEY_DATASET_PATH,
LanceCommonOptions.KEY_NAMESPACE_TYPE)
+ LanceCommonOptions.KEY_DATASET_PATH,
+ LanceCommonOptions.KEY_NAMESPACE_TYPE,
+ notBlank(LanceCommonOptions.KEY_DATASET_PATH),
+ notBlank(LanceCommonOptions.KEY_NAMESPACE_TYPE))
.optional(
LanceCommonOptions.KEY_NAMESPACE_ID,
LanceSinkOptions.WRITE_MAX_ROWS_PER_FILE,
diff --git
a/seatunnel-connectors-v2/connector-lance/src/test/java/org/apache/seatunnel/connectors/seatunnel/lance/LanceFactoryTest.java
b/seatunnel-connectors-v2/connector-lance/src/test/java/org/apache/seatunnel/connectors/seatunnel/lance/LanceFactoryTest.java
index 49ad825322..9d922ac97c 100644
---
a/seatunnel-connectors-v2/connector-lance/src/test/java/org/apache/seatunnel/connectors/seatunnel/lance/LanceFactoryTest.java
+++
b/seatunnel-connectors-v2/connector-lance/src/test/java/org/apache/seatunnel/connectors/seatunnel/lance/LanceFactoryTest.java
@@ -17,15 +17,124 @@
package org.apache.seatunnel.connectors.seatunnel.lance;
+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.lance.config.LanceCommonOptions;
import org.apache.seatunnel.connectors.seatunnel.lance.sink.LanceSinkFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
public class LanceFactoryTest {
+ private final OptionRule optionRule = new LanceSinkFactory().optionRule();
+
@Test
void optionRule() {
- Assertions.assertNotNull((new LanceSinkFactory()).optionRule());
+ Assertions.assertNotNull(optionRule);
+ }
+
+ @Test
+ void testNonblankValuesAccepted() {
+ Map<String, Object> config = validConfig();
+ Assertions.assertDoesNotThrow(() -> validate(config));
+ }
+
+ @Test
+ void testNonblankValuesWithSurroundingWhitespaceAreNotTrimmed() {
+ Map<String, Object> config = validConfig();
+ config.put("dataset_path", " /tmp/test.lance ");
+ config.put("namespace_type", " dir ");
+
+ ReadonlyConfig readonlyConfig = Assertions.assertDoesNotThrow(() ->
validate(config));
+
+ Assertions.assertEquals(
+ " /tmp/test.lance ",
readonlyConfig.get(LanceCommonOptions.KEY_DATASET_PATH));
+ Assertions.assertEquals(" dir ",
readonlyConfig.get(LanceCommonOptions.KEY_NAMESPACE_TYPE));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", "\t", "\n", "\r", " \t\r\n "})
+ void testBlankDatasetPathRejected(String value) {
+ assertInvalidOption("dataset_path", value);
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " ", "\t", "\n", "\r", " \t\r\n "})
+ void testBlankNamespaceTypeRejected(String value) {
+ assertInvalidOption("namespace_type", value);
+ }
+
+ @Test
+ void testOmittedDatasetPathUsesDefault() {
+ ReadonlyConfig config =
+ Assertions.assertDoesNotThrow(
+ () ->
validate(Collections.singletonMap("namespace_type", "dir")));
+
+ Assertions.assertEquals("/test.lance",
config.get(LanceCommonOptions.KEY_DATASET_PATH));
+ }
+
+ @Test
+ void testOmittedNamespaceTypeUsesDefault() {
+ ReadonlyConfig config =
+ Assertions.assertDoesNotThrow(
+ () ->
+ validate(
+ Collections.singletonMap(
+ "dataset_path",
"/tmp/test.lance")));
+
+ Assertions.assertEquals("dir",
config.get(LanceCommonOptions.KEY_NAMESPACE_TYPE));
+ }
+
+ @Test
+ void testOmittedOptionsUseDefaults() {
+ ReadonlyConfig config =
+ Assertions.assertDoesNotThrow(() ->
validate(Collections.emptyMap()));
+
+ Assertions.assertEquals("/test.lance",
config.get(LanceCommonOptions.KEY_DATASET_PATH));
+ Assertions.assertEquals("dir",
config.get(LanceCommonOptions.KEY_NAMESPACE_TYPE));
+ Assertions.assertEquals("",
config.get(LanceCommonOptions.KEY_NAMESPACE_ID));
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"", " \t ", "root"})
+ void testOptionalNamespaceIdPreserved(String value) {
+ ReadonlyConfig config =
+ Assertions.assertDoesNotThrow(
+ () ->
validate(Collections.singletonMap("namespace_id", value)));
+
+ Assertions.assertEquals(value,
config.get(LanceCommonOptions.KEY_NAMESPACE_ID));
+ }
+
+ private void assertInvalidOption(String key, String value) {
+ Map<String, Object> config = validConfig();
+ config.put(key, value);
+
+ OptionValidationException error =
+ Assertions.assertThrows(OptionValidationException.class, () ->
validate(config));
+
+ Assertions.assertTrue(error.getMessage().contains(key),
error.getMessage());
+ }
+
+ private Map<String, Object> validConfig() {
+ Map<String, Object> config = new HashMap<>();
+ config.put("dataset_path", "/tmp/test.lance");
+ config.put("namespace_type", "dir");
+ return config;
+ }
+
+ private ReadonlyConfig validate(Map<String, Object> config) {
+ ReadonlyConfig readonlyConfig = ReadonlyConfig.fromMap(config);
+ ConfigValidator.validateUnknownKeys(readonlyConfig, optionRule,
"Lance");
+ ConfigValidator.of(readonlyConfig).validate(optionRule);
+ return readonlyConfig;
}
}