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 30fb930522 [Improve][Connector-V2] Migrate Kafka Source imperative 
validation to declarative OptionRule (#11157)
30fb930522 is described below

commit 30fb9305220eea28d2e584a5904150a97663ae23
Author: zhiwei.niu <[email protected]>
AuthorDate: Wed Jul 8 22:41:52 2026 +0800

    [Improve][Connector-V2] Migrate Kafka Source imperative validation to 
declarative OptionRule (#11157)
---
 .../seatunnel/kafka/source/KafkaSourceConfig.java  |  28 +--
 .../seatunnel/kafka/source/KafkaSourceFactory.java | 188 ++++++++++++++++--
 .../seatunnel/kafka/KafkaFactoryTest.java          | 220 +++++++++++++++++++++
 .../seatunnel/e2e/connector/kafka/KafkaIT.java     | 121 +++++++++++-
 ...bles_configs_start_mode_earliest_to_assert.conf |  64 ++++++
 ...configs_start_mode_group_offsets_to_assert.conf |  65 ++++++
 ...tables_configs_start_mode_latest_to_assert.conf |  56 ++++++
 ...figs_start_mode_specific_offsets_to_assert.conf |  76 +++++++
 ...les_configs_start_mode_timestamp_to_assert.conf |  65 ++++++
 9 files changed, 847 insertions(+), 36 deletions(-)

diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceConfig.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceConfig.java
index 0913f47ebf..bd689ecea4 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceConfig.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceConfig.java
@@ -41,7 +41,6 @@ import 
org.apache.seatunnel.common.exception.CommonErrorCodeDeprecated;
 import 
org.apache.seatunnel.connectors.seatunnel.kafka.config.KafkaSourceOptions;
 import org.apache.seatunnel.connectors.seatunnel.kafka.config.MessageFormat;
 import 
org.apache.seatunnel.connectors.seatunnel.kafka.config.MessageFormatErrorHandleWay;
-import org.apache.seatunnel.connectors.seatunnel.kafka.config.StartMode;
 import 
org.apache.seatunnel.connectors.seatunnel.kafka.config.TableIdentifierConfig;
 import org.apache.seatunnel.format.avro.AvroDeserializationSchema;
 import 
org.apache.seatunnel.format.compatible.kafka.connect.json.CompatibleKafkaConnectDeserializationSchema;
@@ -59,7 +58,6 @@ import 
org.apache.seatunnel.format.protobuf.SchemaRegistryAwareProtobufDeseriali
 import org.apache.seatunnel.format.text.TextDeserializationSchema;
 import org.apache.seatunnel.format.text.constant.TextFormatConstant;
 
-import org.apache.commons.collections4.MapUtils;
 import org.apache.kafka.common.TopicPartition;
 
 import lombok.Getter;
@@ -132,11 +130,6 @@ public class KafkaSourceConfig implements Serializable {
         this.consumerGroup = readonlyConfig.get(CONSUMER_GROUP);
         this.readerCacheQueueSize = 
readonlyConfig.get(READER_CACHE_QUEUE_SIZE);
         this.ignoreNoLeaderPartition = 
readonlyConfig.get(IGNORE_NO_LEADER_PARTITION);
-        if (this.ignoreNoLeaderPartition && this.discoveryIntervalMillis <= 0) 
{
-            throw new IllegalArgumentException(
-                    "partition-discovery.interval-millis must be configured 
when ignore_no_leader_partition is set to true. "
-                            + "Please provide a positive value for 
partition-discovery.interval-millis.");
-        }
     }
 
     private Properties createKafkaProperties(ReadonlyConfig readonlyConfig) {
@@ -196,10 +189,11 @@ public class KafkaSourceConfig implements Serializable {
                                     long startOffsetsTimestamp =
                                             
readonlyConfig.get(START_MODE_TIMESTAMP);
                                     long currentTimestamp = 
System.currentTimeMillis();
-                                    if (startOffsetsTimestamp < 0
-                                            || startOffsetsTimestamp > 
currentTimestamp) {
+                                    // Runtime check: cannot be declarative 
(depends on current
+                                    // time)
+                                    if (startOffsetsTimestamp > 
currentTimestamp) {
                                         throw new IllegalArgumentException(
-                                                "start_mode.timestamp The 
value is smaller than 0 or smaller than the current time");
+                                                "start_mode.timestamp must not 
be greater than the current time");
                                     }
                                     consumerMetadata.setStartOffsetsTimestamp(
                                             startOffsetsTimestamp);
@@ -207,25 +201,19 @@ public class KafkaSourceConfig implements Serializable {
                                             
readonlyConfig.get(START_MODE_END_TIMESTAMP))) {
                                         long endOffsetsTimestamp =
                                                 
readonlyConfig.get(START_MODE_END_TIMESTAMP);
-                                        if (endOffsetsTimestamp < 0
-                                                || endOffsetsTimestamp > 
currentTimestamp) {
+                                        // Runtime check: cannot be 
declarative (depends on current
+                                        // time)
+                                        if (endOffsetsTimestamp > 
currentTimestamp) {
                                             throw new IllegalArgumentException(
-                                                    "start_mode.endTimestamp 
The value is smaller than 0 or smaller than the current time");
+                                                    "start_mode.end_timestamp 
must not be greater than the current time");
                                         }
                                         
consumerMetadata.setEndOffsetsTimestamp(
                                                 endOffsetsTimestamp);
                                     }
                                     break;
                                 case SPECIFIC_OFFSETS:
-                                    // Key is topic-partition, value is offset
                                     Map<String, Long> offsetMap =
                                             
readonlyConfig.get(START_MODE_OFFSETS);
-                                    if (MapUtils.isEmpty(offsetMap)) {
-                                        throw new IllegalArgumentException(
-                                                "start mode is "
-                                                        + 
StartMode.SPECIFIC_OFFSETS
-                                                        + "but no specific 
offsets were specified.");
-                                    }
                                     Map<TopicPartition, Long> 
specificStartOffsets =
                                             new HashMap<>();
                                     offsetMap.forEach(
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceFactory.java
 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceFactory.java
index 4751d7982a..6e54ea69fa 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceFactory.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/main/java/org/apache/seatunnel/connectors/seatunnel/kafka/source/KafkaSourceFactory.java
@@ -17,7 +17,11 @@
 
 package org.apache.seatunnel.connectors.seatunnel.kafka.source;
 
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConditionExtension;
+import org.apache.seatunnel.api.configuration.util.Conditions;
 import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.configuration.util.OptionValidationException;
 import org.apache.seatunnel.api.source.SeaTunnelSource;
 import org.apache.seatunnel.api.source.SourceSplit;
 import org.apache.seatunnel.api.table.connector.TableSource;
@@ -31,6 +35,8 @@ import 
org.apache.seatunnel.connectors.seatunnel.kafka.config.StartMode;
 import com.google.auto.service.AutoService;
 
 import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
 
 @AutoService(Factory.class)
 public class KafkaSourceFactory implements TableSourceFactory {
@@ -49,7 +55,6 @@ public class KafkaSourceFactory implements TableSourceFactory 
{
                         KafkaSourceOptions.TABLE_CONFIGS,
                         KafkaSourceOptions.TABLE_LIST)
                 .optional(
-                        KafkaSourceOptions.START_MODE,
                         KafkaSourceOptions.PATTERN,
                         KafkaSourceOptions.CONSUMER_GROUP,
                         KafkaSourceOptions.COMMIT_ON_CHECKPOINT,
@@ -60,24 +65,41 @@ public class KafkaSourceFactory implements 
TableSourceFactory {
                         KafkaSourceOptions.DEBEZIUM_RECORD_INCLUDE_SCHEMA,
                         KafkaSourceOptions.DEBEZIUM_RECORD_TABLE_FILTER,
                         
KafkaSourceOptions.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS,
-                        KafkaSourceOptions.READER_CACHE_QUEUE_SIZE,
-                        KafkaSourceOptions.IGNORE_NO_LEADER_PARTITION)
-                .conditional(
-                        KafkaSourceOptions.START_MODE,
-                        StartMode.TIMESTAMP,
-                        KafkaSourceOptions.START_MODE_TIMESTAMP)
-                .conditional(
-                        KafkaSourceOptions.IGNORE_NO_LEADER_PARTITION,
-                        Boolean.TRUE,
-                        
KafkaSourceOptions.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS)
-                .conditional(
-                        KafkaSourceOptions.START_MODE,
-                        StartMode.SPECIFIC_OFFSETS,
-                        KafkaSourceOptions.START_MODE_OFFSETS)
+                        KafkaSourceOptions.READER_CACHE_QUEUE_SIZE)
+                .optional(
+                        KafkaSourceOptions.START_MODE_TIMESTAMP,
+                        Conditions.extension(
+                                KafkaSourceOptions.START_MODE_TIMESTAMP,
+                                new KafkaStartModeTimestampValidator()))
+                .optional(
+                        KafkaSourceOptions.START_MODE_OFFSETS,
+                        Conditions.extension(
+                                KafkaSourceOptions.START_MODE_OFFSETS,
+                                new KafkaStartModeOffsetsValidator()))
+                .optional(
+                        KafkaSourceOptions.START_MODE_END_TIMESTAMP,
+                        
Conditions.greaterOrEqual(KafkaSourceOptions.START_MODE_END_TIMESTAMP, 0L))
+                .optional(
+                        KafkaSourceOptions.TABLE_CONFIGS,
+                        Conditions.extension(
+                                KafkaSourceOptions.TABLE_CONFIGS, new 
KafkaTableConfigsValidator()))
+                .optional(
+                        KafkaSourceOptions.TABLE_LIST,
+                        Conditions.extension(
+                                KafkaSourceOptions.TABLE_LIST, new 
KafkaTableConfigsValidator()))
                 .conditional(
                         KafkaSourceOptions.FORMAT,
                         MessageFormat.PROTOBUF,
                         KafkaSourceOptions.STRIP_SCHEMA_REGISTRY_HEADER)
+                .optional(
+                        KafkaSourceOptions.START_MODE,
+                        Conditions.extension(
+                                KafkaSourceOptions.START_MODE, new 
KafkaStartModeValidator()))
+                .optional(
+                        KafkaSourceOptions.IGNORE_NO_LEADER_PARTITION,
+                        Conditions.extension(
+                                KafkaSourceOptions.IGNORE_NO_LEADER_PARTITION,
+                                new KafkaPartitionDiscoveryValidator()))
                 .build();
     }
 
@@ -91,4 +113,140 @@ public class KafkaSourceFactory implements 
TableSourceFactory {
     public Class<? extends SeaTunnelSource> getSourceClass() {
         return KafkaSource.class;
     }
+
+    private static class KafkaPartitionDiscoveryValidator implements 
ConditionExtension<Boolean> {
+        @Override
+        public String description() {
+            return "If [ignore_no_leader_partition] is true, "
+                    + "then [partition-discovery.interval-millis] must not be 
null and greater than 0";
+        }
+
+        @Override
+        public boolean evaluate(ReadonlyConfig config, Boolean value)
+                throws OptionValidationException {
+            if (!value) {
+                return true;
+            }
+            Long partitionDiscoveryIntervalMillis =
+                    
config.get(KafkaSourceOptions.KEY_PARTITION_DISCOVERY_INTERVAL_MILLIS);
+            return partitionDiscoveryIntervalMillis != null && 
partitionDiscoveryIntervalMillis > 0;
+        }
+    }
+
+    private static class KafkaStartModeValidator implements 
ConditionExtension<StartMode> {
+        @Override
+        public String description() {
+            return "if [start_mode] is timestamp then [start_mode.timestamp] 
must be configured, "
+                    + "if [start_mode] is specific_offsets then 
[start_mode.offsets] must be configured";
+        }
+
+        @Override
+        public boolean evaluate(ReadonlyConfig config, StartMode value)
+                throws OptionValidationException {
+            if (StartMode.TIMESTAMP == value) {
+                return 
config.getOptional(KafkaSourceOptions.START_MODE_TIMESTAMP).isPresent();
+
+            } else if (StartMode.SPECIFIC_OFFSETS == value) {
+                Map<String, Long> startModeOffsets =
+                        config.get(KafkaSourceOptions.START_MODE_OFFSETS);
+                return startModeOffsets != null && !startModeOffsets.isEmpty();
+            }
+            return true;
+        }
+    }
+
+    private static class KafkaStartModeTimestampValidator implements 
ConditionExtension<Long> {
+        @Override
+        public String description() {
+            return "[start_mode.timestamp] is only valid when 
[start_mode]=timestamp and must be >= 0";
+        }
+
+        @Override
+        public boolean evaluate(ReadonlyConfig config, Long value)
+                throws OptionValidationException {
+            StartMode startMode = 
config.getOptional(KafkaSourceOptions.START_MODE).orElse(null);
+            return startMode == StartMode.TIMESTAMP && value != null && value 
>= 0L;
+        }
+    }
+
+    private static class KafkaStartModeOffsetsValidator
+            implements ConditionExtension<Map<String, Long>> {
+        @Override
+        public String description() {
+            return "[start_mode.offsets] is only valid when 
[start_mode]=specific_offsets and must not be empty";
+        }
+
+        @Override
+        public boolean evaluate(ReadonlyConfig config, Map<String, Long> value)
+                throws OptionValidationException {
+            StartMode startMode = 
config.getOptional(KafkaSourceOptions.START_MODE).orElse(null);
+            return startMode == StartMode.SPECIFIC_OFFSETS && value != null && 
!value.isEmpty();
+        }
+    }
+
+    private static class KafkaTableConfigsValidator
+            implements ConditionExtension<List<Map<String, Object>>> {
+
+        @Override
+        public String description() {
+            return "each tables_configs entry: start_mode.timestamp >= 0 when 
TIMESTAMP, "
+                    + "non-empty start_mode.offsets when SPECIFIC_OFFSETS";
+        }
+
+        @Override
+        public boolean evaluate(ReadonlyConfig config, List<Map<String, 
Object>> entries)
+                throws OptionValidationException {
+            if (entries == null || entries.isEmpty()) {
+                return true;
+            }
+            for (int i = 0; i < entries.size(); i++) {
+                ReadonlyConfig entryConfig = 
ReadonlyConfig.fromMap(entries.get(i));
+                StartMode startMode =
+                        
entryConfig.getOptional(KafkaSourceOptions.START_MODE).orElse(null);
+                if (startMode == StartMode.TIMESTAMP) {
+                    Long ts = 
entryConfig.get(KafkaSourceOptions.START_MODE_TIMESTAMP);
+                    if (ts == null || ts < 0) {
+                        throw new OptionValidationException(
+                                "tables_configs[%d]: 'start_mode.timestamp' 
must be >= 0, got: %d",
+                                i, ts);
+                    }
+                    if 
(entries.get(i).containsKey(KafkaSourceOptions.START_MODE_OFFSETS.key())) {
+                        throw new OptionValidationException(
+                                "tables_configs[%d]: 'start_mode.offsets' is 
only valid "
+                                        + "when start_mode=SPECIFIC_OFFSETS",
+                                i);
+                    }
+                    Long endTs = 
entryConfig.get(KafkaSourceOptions.START_MODE_END_TIMESTAMP);
+                    if (endTs != null && endTs < 0) {
+                        throw new OptionValidationException(
+                                "tables_configs[%d]: 
'start_mode.end_timestamp' must be >= 0, got: %d",
+                                i, endTs);
+                    }
+                } else if (startMode == StartMode.SPECIFIC_OFFSETS) {
+                    Map<String, Long> offsets =
+                            
entryConfig.get(KafkaSourceOptions.START_MODE_OFFSETS);
+                    if (offsets == null || offsets.isEmpty()) {
+                        throw new OptionValidationException(
+                                "tables_configs[%d]: 'start_mode.offsets' must 
not be empty "
+                                        + "when start_mode=SPECIFIC_OFFSETS",
+                                i);
+                    }
+                    if 
(entries.get(i).containsKey(KafkaSourceOptions.START_MODE_TIMESTAMP.key())) {
+                        throw new OptionValidationException(
+                                "tables_configs[%d]: 'start_mode.timestamp' is 
only valid "
+                                        + "when start_mode=TIMESTAMP",
+                                i);
+                    }
+                } else if 
(entries.get(i).containsKey(KafkaSourceOptions.START_MODE_TIMESTAMP.key())
+                        || entries.get(i)
+                                
.containsKey(KafkaSourceOptions.START_MODE_OFFSETS.key())) {
+                    throw new OptionValidationException(
+                            "tables_configs[%d]: 'start_mode.timestamp' and "
+                                    + "'start_mode.offsets' require an 
appropriate start_mode",
+                            i);
+                }
+            }
+            return true;
+        }
+    }
 }
diff --git 
a/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaFactoryTest.java
 
b/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaFactoryTest.java
index 7ab57b02f0..6af5fa7937 100644
--- 
a/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaFactoryTest.java
+++ 
b/seatunnel-connectors-v2/connector-kafka/src/test/java/org/apache/seatunnel/connectors/seatunnel/kafka/KafkaFactoryTest.java
@@ -17,17 +17,237 @@
 
 package org.apache.seatunnel.connectors.seatunnel.kafka;
 
+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.kafka.sink.KafkaSinkFactory;
 import 
org.apache.seatunnel.connectors.seatunnel.kafka.source.KafkaSourceFactory;
 
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
 class KafkaFactoryTest {
 
+    private final OptionRule sourceRule = new 
KafkaSourceFactory().optionRule();
+
+    private void validate(Map<String, Object> config) {
+        
ConfigValidator.of(ReadonlyConfig.fromMap(config)).validate(sourceRule);
+    }
+
+    private void validateUnknownKeys(Map<String, Object> config) {
+        ConfigValidator.validateUnknownKeys(
+                ReadonlyConfig.fromMap(config), sourceRule, "KafkaSource");
+    }
+
+    private Map<String, Object> validTimestampConfig() {
+        Map<String, Object> cfg = new HashMap<>();
+        cfg.put("bootstrap.servers", "localhost:9092");
+        cfg.put("topic", "test-topic");
+        cfg.put("start_mode", "TIMESTAMP");
+        cfg.put("start_mode.timestamp", 1000L);
+        return cfg;
+    }
+
+    private Map<String, Object> validSpecificOffsetsConfig() {
+        Map<String, Object> cfg = new HashMap<>();
+        cfg.put("bootstrap.servers", "localhost:9092");
+        cfg.put("topic", "test-topic");
+        cfg.put("start_mode", "SPECIFIC_OFFSETS");
+        Map<String, Long> offsets = new HashMap<>();
+        offsets.put("test-topic-0", 100L);
+        cfg.put("start_mode.offsets", offsets);
+        return cfg;
+    }
+
     @Test
     void optionRule() {
         Assertions.assertNotNull((new KafkaSourceFactory()).optionRule());
         Assertions.assertNotNull((new KafkaSinkFactory()).optionRule());
     }
+
+    @Test
+    void testValidTimestampConfig() {
+        Assertions.assertDoesNotThrow(() -> validate(validTimestampConfig()));
+    }
+
+    @Test
+    void testUnknownKeysAllowsTimestampStartModeOptions() {
+        Assertions.assertDoesNotThrow(() -> 
validateUnknownKeys(validTimestampConfig()));
+    }
+
+    @Test
+    void testNegativeTimestampRejected() {
+        Map<String, Object> cfg = validTimestampConfig();
+        cfg.put("start_mode.timestamp", -1L);
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testTimestampRejectedWithoutTimestampStartMode() {
+        Map<String, Object> cfg = validTimestampConfig();
+        cfg.put("start_mode", "EARLIEST");
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testTimestampRejectedWithoutStartMode() {
+        Map<String, Object> cfg = validTimestampConfig();
+        cfg.remove("start_mode");
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testNegativeEndTimestampRejected() {
+        Map<String, Object> cfg = validTimestampConfig();
+        cfg.put("start_mode.end_timestamp", -1L);
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testValidSpecificOffsetsConfig() {
+        Assertions.assertDoesNotThrow(() -> 
validate(validSpecificOffsetsConfig()));
+    }
+
+    @Test
+    void testUnknownKeysAllowsSpecificOffsetsStartModeOptions() {
+        Assertions.assertDoesNotThrow(() -> 
validateUnknownKeys(validSpecificOffsetsConfig()));
+    }
+
+    @Test
+    void testEmptyOffsetsMapRejected() {
+        Map<String, Object> cfg = new HashMap<>();
+        cfg.put("bootstrap.servers", "localhost:9092");
+        cfg.put("topic", "test-topic");
+        cfg.put("start_mode", "SPECIFIC_OFFSETS");
+        cfg.put("start_mode.offsets", Collections.emptyMap());
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testOffsetsRejectedWithoutSpecificOffsetsStartMode() {
+        Map<String, Object> cfg = validSpecificOffsetsConfig();
+        cfg.put("start_mode", "EARLIEST");
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testOffsetsRejectedWithoutStartMode() {
+        Map<String, Object> cfg = validSpecificOffsetsConfig();
+        cfg.remove("start_mode");
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testIgnoreNoLeaderPartition() {
+        Map<String, Object> cfg = new HashMap<>();
+        cfg.put("bootstrap.servers", "localhost:9092");
+        cfg.put("topic", "test-topic");
+        cfg.put("ignore_no_leader_partition", false);
+        Assertions.assertDoesNotThrow(() -> validate(cfg));
+
+        cfg.put("ignore_no_leader_partition", true);
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+
+        cfg.put("partition-discovery.interval-millis", 0);
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+
+        cfg.put("partition-discovery.interval-millis", 10);
+        Assertions.assertDoesNotThrow(() -> validate(cfg));
+    }
+
+    // --- Multi-table (tables_configs) tests ---
+
+    private Map<String, Object> validMultiTableConfig() {
+        Map<String, Object> cfg = new HashMap<>();
+        cfg.put("bootstrap.servers", "localhost:9092");
+
+        Map<String, Object> entry = new HashMap<>();
+        entry.put("topic", "multi-topic");
+        entry.put("start_mode", "TIMESTAMP");
+        entry.put("start_mode.timestamp", 5000L);
+        List<Map<String, Object>> tables = new ArrayList<>();
+        tables.add(entry);
+        cfg.put("tables_configs", tables);
+        return cfg;
+    }
+
+    private Map<String, Object> validTableListConfig() {
+        Map<String, Object> cfg = validMultiTableConfig();
+        cfg.put("table_list", cfg.remove("tables_configs"));
+        return cfg;
+    }
+
+    @Test
+    void testMultiTableValidConfig() {
+        Assertions.assertDoesNotThrow(() -> validate(validMultiTableConfig()));
+    }
+
+    @Test
+    void testTableListValidConfig() {
+        Assertions.assertDoesNotThrow(() -> validate(validTableListConfig()));
+    }
+
+    @Test
+    void testMultiTableNegativeTimestampRejected() {
+        Map<String, Object> cfg = validMultiTableConfig();
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> tables = (List<Map<String, Object>>) 
cfg.get("tables_configs");
+        tables.get(0).put("start_mode.timestamp", null);
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testMultiTableNegativeEndTimestampRejected() {
+        Map<String, Object> cfg = validMultiTableConfig();
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> tables = (List<Map<String, Object>>) 
cfg.get("tables_configs");
+        tables.get(0).put("start_mode.end_timestamp", -1L);
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testMultiTableTimestampRejectedWithoutTimestampStartMode() {
+        Map<String, Object> cfg = validMultiTableConfig();
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> tables = (List<Map<String, Object>>) 
cfg.get("tables_configs");
+        tables.get(0).put("start_mode", "EARLIEST");
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testMultiTableEmptyOffsetsRejected() {
+        Map<String, Object> cfg = new HashMap<>();
+        cfg.put("bootstrap.servers", "localhost:9092");
+
+        Map<String, Object> entry = new HashMap<>();
+        entry.put("topic", "multi-topic");
+        entry.put("start_mode", "SPECIFIC_OFFSETS");
+        entry.put("start_mode.offsets", Collections.emptyMap());
+        List<Map<String, Object>> tables = new ArrayList<>();
+        tables.add(entry);
+        cfg.put("tables_configs", tables);
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
+
+    @Test
+    void testMultiTableOffsetsRejectedWithoutSpecificOffsetsStartMode() {
+        Map<String, Object> cfg = new HashMap<>();
+        cfg.put("bootstrap.servers", "localhost:9092");
+
+        Map<String, Object> entry = new HashMap<>();
+        entry.put("topic", "multi-topic");
+        entry.put("start_mode", "EARLIEST");
+        entry.put("start_mode.offsets", 
Collections.singletonMap("multi-topic-0", 1L));
+        List<Map<String, Object>> tables = new ArrayList<>();
+        tables.add(entry);
+        cfg.put("tables_configs", tables);
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validate(cfg));
+    }
 }
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
index 72ac59dc22..751031dca6 100644
--- 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/java/org/apache/seatunnel/e2e/connector/kafka/KafkaIT.java
@@ -106,12 +106,14 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
@@ -143,6 +145,8 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
     /** Topics created dynamically during tests; cleaned up in {@link 
#tearDown()}. */
     private final List<String> dynamicTopics = new CopyOnWriteArrayList<>();
 
+    private final AtomicInteger startModeTestSequence = new AtomicInteger();
+
     @BeforeAll
     @Override
     public void startUp() throws Exception {
@@ -1119,6 +1123,96 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
         Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
     }
 
+    @TestTemplate
+    public void testSourceKafkaTablesConfigsEarliestStartMode(TestContainer 
container)
+            throws IOException, InterruptedException {
+        String topic = startModeTopic(container, "earliest");
+        createKafkaTopic(topic);
+        generateSimpleJsonRecords(topic, 0, 10);
+
+        Container.ExecResult execResult =
+                container.executeJob(
+                        
"/kafka/kafkasource_tables_configs_start_mode_earliest_to_assert.conf",
+                        Collections.singletonList("topic=" + topic));
+        Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+    }
+
+    @TestTemplate
+    public void testSourceKafkaTablesConfigsLatestStartMode(TestContainer 
container)
+            throws IOException, InterruptedException {
+        String topic = startModeTopic(container, "latest");
+        createKafkaTopic(topic);
+        generateSimpleJsonRecords(topic, 0, 10);
+
+        Container.ExecResult execResult =
+                container.executeJob(
+                        
"/kafka/kafkasource_tables_configs_start_mode_latest_to_assert.conf",
+                        Collections.singletonList("topic=" + topic));
+        Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+    }
+
+    @TestTemplate
+    public void 
testSourceKafkaTablesConfigsGroupOffsetsStartMode(TestContainer container)
+            throws IOException, InterruptedException {
+        String topic = startModeTopic(container, "group_offsets");
+        String consumerGroup = "SeaTunnel-Start-Mode-Tables-Group-Offsets";
+        createKafkaTopic(topic);
+        generateSimpleJsonRecords(topic, 0, 5);
+        commitOffset(topic, consumerGroup);
+        generateSimpleJsonRecords(topic, 5, 10);
+
+        Container.ExecResult execResult =
+                container.executeJob(
+                        
"/kafka/kafkasource_tables_configs_start_mode_group_offsets_to_assert.conf",
+                        Collections.singletonList("topic=" + topic));
+        Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+    }
+
+    @TestTemplate
+    public void testSourceKafkaTablesConfigsTimestampStartMode(TestContainer 
container)
+            throws IOException, InterruptedException {
+        String topic = startModeTopic(container, "timestamp");
+        createKafkaTopic(topic);
+        generateSimpleJsonRecordsWithTimestamp(topic, 0, 4, 1738395839000L);
+        generateSimpleJsonRecordsWithTimestamp(topic, 4, 10, 1738395840000L);
+
+        Container.ExecResult execResult =
+                container.executeJob(
+                        
"/kafka/kafkasource_tables_configs_start_mode_timestamp_to_assert.conf",
+                        Collections.singletonList("topic=" + topic));
+        Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+    }
+
+    @TestTemplate
+    public void 
testSourceKafkaTablesConfigsSpecificOffsetsStartMode(TestContainer container)
+            throws IOException, InterruptedException {
+        String topic = fixedStartModeTopic(container, "specific_offsets");
+        createKafkaTopic(topic);
+        generateSimpleJsonRecords(topic, 0, 10);
+
+        Container.ExecResult execResult =
+                container.executeJob(
+                        
"/kafka/kafkasource_tables_configs_start_mode_specific_offsets_to_assert.conf",
+                        Collections.singletonList("topic=" + topic));
+        Assertions.assertEquals(0, execResult.getExitCode(), 
execResult.getStderr());
+    }
+
+    private String fixedStartModeTopic(TestContainer container, String 
startMode) {
+        return "test_topic_start_mode_"
+                + container.identifier().name().toLowerCase(Locale.ROOT)
+                + "_"
+                + startMode;
+    }
+
+    private String startModeTopic(TestContainer container, String startMode) {
+        return "test_topic_start_mode_"
+                + container.identifier().name().toLowerCase(Locale.ROOT)
+                + "_"
+                + startMode
+                + "_"
+                + startModeTestSequence.incrementAndGet();
+    }
+
     @TestTemplate
     public void testSourceKafkaStartConfig(TestContainer container)
             throws IOException, InterruptedException {
@@ -1135,6 +1229,31 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
         testKafkaGroupOffsetsToConsole(container);
     }
 
+    private void generateSimpleJsonRecords(String topic, int startInclusive, 
int endExclusive) {
+        generateSimpleJsonRecordsWithTimestamp(topic, startInclusive, 
endExclusive, null);
+    }
+
+    private void generateSimpleJsonRecordsWithTimestamp(
+            String topic, int startInclusive, int endExclusive, Long 
timestamp) {
+        try {
+            for (int i = startInclusive; i < endExclusive; i++) {
+                byte[] value = ("{\"id\":" + i + 
"}").getBytes(StandardCharsets.UTF_8);
+                ProducerRecord<byte[], byte[]> record =
+                        timestamp == null
+                                ? new ProducerRecord<>(topic, null, value)
+                                : new ProducerRecord<>(topic, null, timestamp, 
null, value);
+                producer.send(record).get();
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException("Interrupted while writing Kafka 
records to " + topic, e);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to write Kafka start mode 
records to " + topic, e);
+        } finally {
+            producer.flush();
+        }
+    }
+
     public void commitOffset(String topic, String groupId) {
         Properties props = new Properties();
         props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, 
kafkaContainer.getBootstrapServers());
@@ -1712,7 +1831,7 @@ public class KafkaIT extends TestSuiteBase implements 
TestResource {
                     return null;
                 });
         // wait for data written to kafka
-        given().pollDelay(60, SECONDS)
+        given().pollDelay(120, SECONDS)
                 .pollInterval(5, SECONDS)
                 .await()
                 .atMost(5, MINUTES)
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_earliest_to_assert.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_earliest_to_assert.conf
new file mode 100644
index 0000000000..cfe7b7692f
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_earliest_to_assert.conf
@@ -0,0 +1,64 @@
+#
+# 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 {
+  Kafka {
+    bootstrap.servers = "kafkaCluster:9092"
+    format = json
+    tables_configs = [
+      {
+        topic = "${topic}"
+        start_mode = earliest
+        schema = {
+          table = "kafka_start_mode.earliest"
+          fields {
+            id = bigint
+          }
+        }
+      }
+    ]
+  }
+}
+
+sink {
+  Assert {
+    rules = {
+      tables_configs = [
+        {
+          table_path = "kafka_start_mode.earliest"
+          row_rules = [
+            { rule_type = MIN_ROW, rule_value = 10 },
+            { rule_type = MAX_ROW, rule_value = 10 }
+          ]
+          field_rules = [{
+            field_name = id
+            field_type = bigint
+            field_value = [
+              { rule_type = MIN, rule_value = 0 },
+              { rule_type = MAX, rule_value = 9 }
+            ]
+          }]
+        }
+      ]
+    }
+  }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_group_offsets_to_assert.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_group_offsets_to_assert.conf
new file mode 100644
index 0000000000..ab6e7b8cb2
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_group_offsets_to_assert.conf
@@ -0,0 +1,65 @@
+#
+# 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 {
+  Kafka {
+    bootstrap.servers = "kafkaCluster:9092"
+    format = json
+    consumer.group = "SeaTunnel-Start-Mode-Tables-Group-Offsets"
+    tables_configs = [
+      {
+        topic = "${topic}"
+        start_mode = group_offsets
+        schema = {
+          table = "kafka_start_mode.group_offsets"
+          fields {
+            id = bigint
+          }
+        }
+      }
+    ]
+  }
+}
+
+sink {
+  Assert {
+    rules = {
+      tables_configs = [
+        {
+          table_path = "kafka_start_mode.group_offsets"
+          row_rules = [
+            { rule_type = MIN_ROW, rule_value = 5 },
+            { rule_type = MAX_ROW, rule_value = 5 }
+          ]
+          field_rules = [{
+            field_name = id
+            field_type = bigint
+            field_value = [
+              { rule_type = MIN, rule_value = 5 },
+              { rule_type = MAX, rule_value = 9 }
+            ]
+          }]
+        }
+      ]
+    }
+  }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_latest_to_assert.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_latest_to_assert.conf
new file mode 100644
index 0000000000..4d32b17800
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_latest_to_assert.conf
@@ -0,0 +1,56 @@
+#
+# 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 {
+  Kafka {
+    bootstrap.servers = "kafkaCluster:9092"
+    format = json
+    tables_configs = [
+      {
+        topic = "${topic}"
+        start_mode = latest
+        schema = {
+          table = "kafka_start_mode.latest"
+          fields {
+            id = bigint
+          }
+        }
+      }
+    ]
+  }
+}
+
+sink {
+  Assert {
+    rules = {
+      tables_configs = [
+        {
+          table_path = "kafka_start_mode.latest"
+          row_rules = [
+            { rule_type = MIN_ROW, rule_value = 0 },
+            { rule_type = MAX_ROW, rule_value = 0 }
+          ]
+        }
+      ]
+    }
+  }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_specific_offsets_to_assert.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_specific_offsets_to_assert.conf
new file mode 100644
index 0000000000..d7290a5032
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_specific_offsets_to_assert.conf
@@ -0,0 +1,76 @@
+#
+# 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 {
+  Kafka {
+    bootstrap.servers = "kafkaCluster:9092"
+    format = json
+    tables_configs = [
+      {
+        topic = "${topic}"
+        start_mode = specific_offsets
+        start_mode.offsets = {
+          test_topic_start_mode_flink_1_13_specific_offsets-0 = 5
+          test_topic_start_mode_flink_1_14_specific_offsets-0 = 5
+          test_topic_start_mode_flink_1_15_specific_offsets-0 = 5
+          test_topic_start_mode_flink_1_16_specific_offsets-0 = 5
+          test_topic_start_mode_flink_1_17_specific_offsets-0 = 5
+          test_topic_start_mode_flink_1_18_specific_offsets-0 = 5
+          test_topic_start_mode_flink_1_20_specific_offsets-0 = 5
+          test_topic_start_mode_spark_2_4_specific_offsets-0 = 5
+          test_topic_start_mode_spark_3_3_specific_offsets-0 = 5
+          test_topic_start_mode_seatunnel_specific_offsets-0 = 5
+        }
+        schema = {
+          table = "kafka_start_mode.specific_offsets"
+          fields {
+            id = bigint
+          }
+        }
+      }
+    ]
+  }
+}
+
+sink {
+  Assert {
+    rules = {
+      tables_configs = [
+        {
+          table_path = "kafka_start_mode.specific_offsets"
+          row_rules = [
+            { rule_type = MIN_ROW, rule_value = 5 },
+            { rule_type = MAX_ROW, rule_value = 5 }
+          ]
+          field_rules = [{
+            field_name = id
+            field_type = bigint
+            field_value = [
+              { rule_type = MIN, rule_value = 5 },
+              { rule_type = MAX, rule_value = 9 }
+            ]
+          }]
+        }
+      ]
+    }
+  }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_timestamp_to_assert.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_timestamp_to_assert.conf
new file mode 100644
index 0000000000..b4599877b6
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-kafka-e2e/src/test/resources/kafka/kafkasource_tables_configs_start_mode_timestamp_to_assert.conf
@@ -0,0 +1,65 @@
+#
+# 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 {
+  Kafka {
+    bootstrap.servers = "kafkaCluster:9092"
+    format = json
+    tables_configs = [
+      {
+        topic = "${topic}"
+        start_mode = timestamp
+        start_mode.timestamp = 1738395840000
+        schema = {
+          table = "kafka_start_mode.timestamp"
+          fields {
+            id = bigint
+          }
+        }
+      }
+    ]
+  }
+}
+
+sink {
+  Assert {
+    rules = {
+      tables_configs = [
+        {
+          table_path = "kafka_start_mode.timestamp"
+          row_rules = [
+            { rule_type = MIN_ROW, rule_value = 6 },
+            { rule_type = MAX_ROW, rule_value = 6 }
+          ]
+          field_rules = [{
+            field_name = id
+            field_type = bigint
+            field_value = [
+              { rule_type = MIN, rule_value = 4 },
+              { rule_type = MAX, rule_value = 9 }
+            ]
+          }]
+        }
+      ]
+    }
+  }
+}

Reply via email to