This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/dev/pr-12404-463adf8c47aab1f9da8f135121917281870d9b97 in repository https://gitbox.apache.org/repos/asf/seatunnel.git
commit 4cf17541df318472b5fffdfcbaab2b1a33ec31c8 Author: TianHengZhuang <[email protected]> AuthorDate: Tue Sep 29 13:31:39 2026 +0000 [Improve][Connector-V2][Fake] Declarative optionRule value constraints for fake source (#12404) --- .../introduction/concepts/incompatible-changes.md | 12 ++ .../introduction/concepts/incompatible-changes.md | 9 + .../seatunnel/fake/source/FakeSourceFactory.java | 53 ++++- .../seatunnel/fake/source/FakeFactoryTest.java | 228 ++++++++++++++++++++- 4 files changed, 293 insertions(+), 9 deletions(-) diff --git a/docs/en/introduction/concepts/incompatible-changes.md b/docs/en/introduction/concepts/incompatible-changes.md index 3d1e871d57..5719f6fa40 100644 --- a/docs/en/introduction/concepts/incompatible-changes.md +++ b/docs/en/introduction/concepts/incompatible-changes.md @@ -52,6 +52,18 @@ You need to check this document before you upgrade to related version. trust store of the SeaTunnel runtime, or switch to the `host`/`port` + `ssl = true` configuration with a properly configured trust store. +### FakeSource (connector-fake) + +- Declarative option constraints are now enforced at factory validation time instead of + silently passing and failing only at runtime. Affected options: `split.num`, + `vector.dimension` and `binary.vector.dimension` must be > 0; `row.num`, + `split.read-interval`, `map.size`, `array.size`, `bytes.length` and `string.length` must be + >= 0; `tinyint.min/max`, `smallint.min/max`, `int.min/max`, `bigint.min/max`, + `float.min/max`, `double.min/max` and `vector.float.min/max` must satisfy min <= max. + Note that `row.num = 0` (empty source) is still valid. Existing jobs that set invalid + values and previously ran successfully will now fail fast at startup with a validation + error. + ### Zeta REST Pagination Parameter Validation - **Behavior change: `page` and `rows` are validated on paginated endpoints** diff --git a/docs/zh/introduction/concepts/incompatible-changes.md b/docs/zh/introduction/concepts/incompatible-changes.md index 90384b9c00..17912d0650 100644 --- a/docs/zh/introduction/concepts/incompatible-changes.md +++ b/docs/zh/introduction/concepts/incompatible-changes.md @@ -26,6 +26,15 @@ - **迁移指南**:将 Broker 证书(或私有 CA 证书链)导入 SeaTunnel 运行时的 JVM 信任库,或改用 `host`/`port` + `ssl = true` 配置并正确设置信任库。 +### FakeSource (connector-fake) + +- 声明式选项约束现在在工厂校验阶段即强制生效,而不再静默放行、直到运行时才失败。受影响选项:`split.num`、 + `vector.dimension` 和 `binary.vector.dimension` 必须 > 0;`row.num`、`split.read-interval`、`map.size`、 + `array.size`、`bytes.length` 和 `string.length` 必须 >= 0;`tinyint.min/max`、`smallint.min/max`、 + `int.min/max`、`bigint.min/max`、`float.min/max`、`double.min/max` 和 `vector.float.min/max` + 必须满足 min <= max。注意 `row.num = 0`(空 Source)仍然有效。此前设置了无效值且成功运行的现有作业, + 将在启动时快速抛出校验错误并失败。 + ### Zeta REST 分页参数校验 - **行为变更:分页接口开始校验 `page` 与 `rows`** diff --git a/seatunnel-connectors-v2/connector-fake/src/main/java/org/apache/seatunnel/connectors/seatunnel/fake/source/FakeSourceFactory.java b/seatunnel-connectors-v2/connector-fake/src/main/java/org/apache/seatunnel/connectors/seatunnel/fake/source/FakeSourceFactory.java index 556b5bf5a2..5bf75cfc07 100644 --- a/seatunnel-connectors-v2/connector-fake/src/main/java/org/apache/seatunnel/connectors/seatunnel/fake/source/FakeSourceFactory.java +++ b/seatunnel-connectors-v2/connector-fake/src/main/java/org/apache/seatunnel/connectors/seatunnel/fake/source/FakeSourceFactory.java @@ -17,6 +17,7 @@ package org.apache.seatunnel.connectors.seatunnel.fake.source; +import org.apache.seatunnel.api.configuration.util.Conditions; import org.apache.seatunnel.api.configuration.util.OptionRule; import org.apache.seatunnel.api.options.ConnectorCommonOptions; import org.apache.seatunnel.api.source.SeaTunnelSource; @@ -39,6 +40,8 @@ import java.util.stream.Collectors; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.ARRAY_SIZE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.BIGINT_FAKE_MODE; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.BIGINT_MAX; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.BIGINT_MIN; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.BIGINT_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.BINARY_VECTOR_DIMENSION; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.BYTES_LENGTH; @@ -46,26 +49,39 @@ import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOp import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.DATE_MONTH_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.DATE_YEAR_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.DOUBLE_FAKE_MODE; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.DOUBLE_MAX; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.DOUBLE_MIN; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.DOUBLE_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.FLOAT_FAKE_MODE; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.FLOAT_MAX; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.FLOAT_MIN; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.FLOAT_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.INT_FAKE_MODE; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.INT_MAX; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.INT_MIN; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.INT_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.MAP_SIZE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.ROWS; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.ROW_NUM; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.SMALLINT_FAKE_MODE; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.SMALLINT_MAX; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.SMALLINT_MIN; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.SMALLINT_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.SPLIT_NUM; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.SPLIT_READ_INTERVAL; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.STRING_FAKE_MODE; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.STRING_LENGTH; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.STRING_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.TIME_HOUR_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.TIME_MINUTE_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.TIME_SECOND_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.TINYINT_FAKE_MODE; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.TINYINT_MAX; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.TINYINT_MIN; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.TINYINT_TEMPLATE; import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.VECTOR_DIMENSION; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.VECTOR_FLOAT_MAX; +import static org.apache.seatunnel.connectors.seatunnel.fake.config.FakeSourceOptions.VECTOR_FLOAT_MIN; @AutoService(Factory.class) public class FakeSourceFactory implements TableSourceFactory, SupportSourceDryRunValidation { @@ -78,6 +94,35 @@ public class FakeSourceFactory implements TableSourceFactory, SupportSourceDryRu public OptionRule optionRule() { return OptionRule.builder() .exclusive(ConnectorCommonOptions.TABLE_CONFIGS, ConnectorCommonOptions.SCHEMA) + // row.num=0 stays valid (empty source); split.num must be positive + .optional(ROW_NUM, Conditions.greaterOrEqual(ROW_NUM, 0)) + .optional(SPLIT_NUM, Conditions.greaterThan(SPLIT_NUM, 0)) + .optional(SPLIT_READ_INTERVAL, Conditions.greaterOrEqual(SPLIT_READ_INTERVAL, 0)) + .optional(MAP_SIZE, Conditions.greaterOrEqual(MAP_SIZE, 0)) + .optional(ARRAY_SIZE, Conditions.greaterOrEqual(ARRAY_SIZE, 0)) + .optional(BYTES_LENGTH, Conditions.greaterOrEqual(BYTES_LENGTH, 0)) + .optional(STRING_LENGTH, Conditions.greaterOrEqual(STRING_LENGTH, 0)) + .optional(VECTOR_DIMENSION, Conditions.greaterThan(VECTOR_DIMENSION, 0)) + .optional( + BINARY_VECTOR_DIMENSION, Conditions.greaterThan(BINARY_VECTOR_DIMENSION, 0)) + .optional( + TINYINT_MIN, + TINYINT_MAX, + Conditions.lessOrEqualField(TINYINT_MIN, TINYINT_MAX)) + .optional( + SMALLINT_MIN, + SMALLINT_MAX, + Conditions.lessOrEqualField(SMALLINT_MIN, SMALLINT_MAX)) + .optional(INT_MIN, INT_MAX, Conditions.lessOrEqualField(INT_MIN, INT_MAX)) + .optional( + BIGINT_MIN, BIGINT_MAX, Conditions.lessOrEqualField(BIGINT_MIN, BIGINT_MAX)) + .optional(FLOAT_MIN, FLOAT_MAX, Conditions.lessOrEqualField(FLOAT_MIN, FLOAT_MAX)) + .optional( + DOUBLE_MIN, DOUBLE_MAX, Conditions.lessOrEqualField(DOUBLE_MIN, DOUBLE_MAX)) + .optional( + VECTOR_FLOAT_MIN, + VECTOR_FLOAT_MAX, + Conditions.lessOrEqualField(VECTOR_FLOAT_MIN, VECTOR_FLOAT_MAX)) .optional( STRING_FAKE_MODE, TINYINT_FAKE_MODE, @@ -87,14 +132,6 @@ public class FakeSourceFactory implements TableSourceFactory, SupportSourceDryRu FLOAT_FAKE_MODE, DOUBLE_FAKE_MODE, ROWS, - ROW_NUM, - SPLIT_NUM, - SPLIT_READ_INTERVAL, - MAP_SIZE, - ARRAY_SIZE, - BYTES_LENGTH, - VECTOR_DIMENSION, - BINARY_VECTOR_DIMENSION, DATE_YEAR_TEMPLATE, DATE_MONTH_TEMPLATE, DATE_DAY_TEMPLATE, diff --git a/seatunnel-connectors-v2/connector-fake/src/test/java/org/apache/seatunnel/connectors/seatunnel/fake/source/FakeFactoryTest.java b/seatunnel-connectors-v2/connector-fake/src/test/java/org/apache/seatunnel/connectors/seatunnel/fake/source/FakeFactoryTest.java index 6dd59e7c39..c35c6d1807 100644 --- a/seatunnel-connectors-v2/connector-fake/src/test/java/org/apache/seatunnel/connectors/seatunnel/fake/source/FakeFactoryTest.java +++ b/seatunnel-connectors-v2/connector-fake/src/test/java/org/apache/seatunnel/connectors/seatunnel/fake/source/FakeFactoryTest.java @@ -17,13 +17,239 @@ package org.apache.seatunnel.connectors.seatunnel.fake.source; +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.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + public class FakeFactoryTest { + private final FakeSourceFactory factory = new FakeSourceFactory(); + + private static Map<String, Object> validBaseConfig() { + Map<String, Object> map = new HashMap<>(); + Map<String, Object> schema = new HashMap<>(); + schema.put("fields", new HashMap<String, Object>()); + map.put("schema", schema); + return map; + } + + private void validate(Map<String, Object> map) { + ConfigValidator.of(ReadonlyConfig.fromMap(map)).validate(factory.optionRule()); + } + @Test void optionRule() { - Assertions.assertNotNull((new FakeSourceFactory()).optionRule()); + Assertions.assertNotNull(factory.optionRule()); + } + + @Test + void validConfigPassesValidation() { + validate(validBaseConfig()); + } + + @Test + void validNumericBoundsPassValidation() { + Map<String, Object> map = validBaseConfig(); + map.put("row.num", 1); + map.put("split.num", 1); + map.put("split.read-interval", 0); + map.put("map.size", 0); + map.put("array.size", 0); + map.put("bytes.length", 0); + map.put("string.length", 0); + map.put("vector.dimension", 1); + map.put("binary.vector.dimension", 8); + validate(map); + } + + @Test + void rowNumZeroPassesValidation() { + Map<String, Object> map = validBaseConfig(); + map.put("row.num", 0); + validate(map); + } + + @Test + void minEqualsMaxPassesValidation() { + Map<String, Object> map = validBaseConfig(); + map.put("int.min", 5); + map.put("int.max", 5); + map.put("tinyint.min", 1); + map.put("tinyint.max", 1); + map.put("smallint.min", 2); + map.put("smallint.max", 2); + map.put("bigint.min", 3L); + map.put("bigint.max", 3L); + map.put("float.min", 1.5f); + map.put("float.max", 1.5f); + map.put("double.min", 2.5d); + map.put("double.max", 2.5d); + map.put("vector.float.min", 0.5f); + map.put("vector.float.max", 0.5f); + validate(map); + } + + @Test + void oneSidedMinOrMaxPassesValidation() { + Map<String, Object> onlyMin = validBaseConfig(); + onlyMin.put("int.min", 0); + validate(onlyMin); + + Map<String, Object> onlyMax = validBaseConfig(); + onlyMax.put("int.max", 100); + validate(onlyMax); + } + + @Test + void exclusiveUsesListForTableConfigs() { + Map<String, Object> map = validBaseConfig(); + List<Map<String, Object>> tables = Collections.singletonList(new HashMap<>()); + map.put("tables_configs", tables); + OptionValidationException ex = + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + String message = ex.getMessage(); + Assertions.assertTrue( + message.contains("schema") || message.contains("tables_configs"), + () -> "expected exclusive message, got: " + message); + } + + @Test + void neitherSchemaNorTableConfigsRejected() { + Assertions.assertThrows(OptionValidationException.class, () -> validate(new HashMap<>())); + } + + @Test + void nonPositiveSplitNumIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("split.num", 0); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void negativeMapSizeIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("map.size", -1); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void negativeArraySizeIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("array.size", -1); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void negativeBytesLengthIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("bytes.length", -1); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void negativeStringLengthIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("string.length", -1); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void negativeSplitReadIntervalIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("split.read-interval", -1); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void nonPositiveVectorDimensionIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("vector.dimension", 0); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void nonPositiveBinaryVectorDimensionIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("binary.vector.dimension", 0); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void tinyintMinGreaterThanMaxIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("tinyint.min", 10); + map.put("tinyint.max", 5); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void smallintMinGreaterThanMaxIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("smallint.min", 10); + map.put("smallint.max", 5); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void intMinGreaterThanIntMaxIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("int.min", 10); + map.put("int.max", 5); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void bigintMinGreaterThanMaxIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("bigint.min", 10L); + map.put("bigint.max", 5L); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void floatMinGreaterThanMaxIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("float.min", 2.0f); + map.put("float.max", 1.0f); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void doubleMinGreaterThanMaxIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("double.min", 2.0d); + map.put("double.max", 1.0d); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void vectorFloatMinGreaterThanMaxIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("vector.float.min", 2.0f); + map.put("vector.float.max", 1.0f); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void templateModeWithoutTemplateIsRejected() { + Map<String, Object> map = validBaseConfig(); + map.put("string.fake.mode", "TEMPLATE"); + Assertions.assertThrows(OptionValidationException.class, () -> validate(map)); + } + + @Test + void templateModeWithTemplatePassesValidation() { + Map<String, Object> map = validBaseConfig(); + map.put("string.fake.mode", "TEMPLATE"); + map.put("string.template", Collections.singletonList("hello")); + validate(map); } }
