This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] 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 6c5b5b630f [Improve][Connector-V2][Neo4j] Migrate configuration 
validation to OptionRule (#12047)
6c5b5b630f is described below

commit 6c5b5b630fa6c7b6322b522b0aa5229fd528b032
Author: 郑朝钦 <[email protected]>
AuthorDate: Thu Sep 3 13:50:18 2026 +0000

    [Improve][Connector-V2][Neo4j] Migrate configuration validation to 
OptionRule (#12047)
    
    Signed-off-by: jjj-n <[email protected]>
---
 docs/en/connectors/sink/Neo4j.md                   |  10 +-
 docs/zh/connectors/sink/Neo4j.md                   |  10 +-
 .../config/Neo4jAuthenticationConditions.java      |  60 +++++++++
 .../seatunnel/neo4j/config/Neo4jQueryInfo.java     |  58 +-------
 .../seatunnel/neo4j/config/Neo4jSinkQueryInfo.java |  31 +----
 .../neo4j/config/Neo4jSourceQueryInfo.java         |   4 +-
 .../seatunnel/neo4j/sink/Neo4jSinkFactory.java     |  26 +++-
 .../seatunnel/neo4j/source/Neo4jSourceFactory.java |  12 +-
 .../seatunnel/neo4j/Neo4jFactoryTest.java          | 149 +++++++++++++++++++++
 9 files changed, 254 insertions(+), 106 deletions(-)

diff --git a/docs/en/connectors/sink/Neo4j.md b/docs/en/connectors/sink/Neo4j.md
index dba46fc580..b3470d1214 100644
--- a/docs/en/connectors/sink/Neo4j.md
+++ b/docs/en/connectors/sink/Neo4j.md
@@ -32,7 +32,7 @@ It supports one-row-at-a-time writes and batch writes with 
Cypher `UNWIND`.
 | kerberos_ticket            | String  | No       | -          | Kerberos 
ticket used for Neo4j authentication.                                           
                                                            |
 | database                   | String  | Yes      | -          | Neo4j 
database name.                                                                  
                                                               |
 | query                      | String  | Yes      | -          | Cypher 
statement used to write data. In `ONE_BY_ONE` mode, use placeholders such as 
`$name`; in `BATCH` mode, use `UNWIND $batch AS row`.            |
-| queryParamPosition         | Object  | Yes      | -          | Mapping 
between Cypher parameter names and input row field positions. It is required by 
the connector configuration.                                  |
+| queryParamPosition         | Object  | ONE_BY_ONE only | -          | 
Mapping between Cypher parameter names and input row field positions. Required 
when `write_mode = "ONE_BY_ONE"`.                                    |
 | max_batch_size             | Integer | No       | 500        | Maximum 
number of rows written in one transaction when `write_mode = "BATCH"`. Must be 
greater than 0.                                                |
 | write_mode                 | String  | No       | ONE_BY_ONE | Write mode. 
Supported values are `ONE_BY_ONE` and `BATCH`.                                  
                                                          |
 | max_transaction_retry_time | Long    | No       | 30         | Maximum 
transaction retry time, in seconds.                                             
                                                             |
@@ -41,10 +41,9 @@ It supports one-row-at-a-time writes and batch writes with 
Cypher `UNWIND`.
 
 ## Notes
 
-- Use exactly one authentication method: username/password, bearer token, or 
Kerberos ticket.
+- Configure at least one authentication method: username/password, bearer 
token, or Kerberos ticket. If several are configured, username/password takes 
precedence, followed by bearer token and Kerberos ticket.
 - In `ONE_BY_ONE` mode, `queryParamPosition` maps each Cypher placeholder to a 
field position in the input row.
 - In `BATCH` mode, the query should use `UNWIND $batch AS row`. The connector 
passes the rows through the `batch` variable.
-- `queryParamPosition` is still required by the connector configuration in 
`BATCH` mode, even though the batch query reads values from `row`.
 - Field positions in `queryParamPosition` start from `0`, following the input 
schema field order.
 - In `BATCH` mode, each `row` entry uses the input field names, so the names 
referenced in the Cypher statement must match the upstream schema.
 
@@ -104,11 +103,6 @@ sink {
     max_transaction_retry_time = 3
     max_connection_timeout = 10
 
-    queryParamPosition = {
-      name = 0
-      age = 1
-    }
-
     query = "UNWIND $batch AS row CREATE (n:BatchLabel) SET n.name = row.name, 
n.age = row.age"
   }
 }
diff --git a/docs/zh/connectors/sink/Neo4j.md b/docs/zh/connectors/sink/Neo4j.md
index b5c76ad722..eaf3fca2c3 100644
--- a/docs/zh/connectors/sink/Neo4j.md
+++ b/docs/zh/connectors/sink/Neo4j.md
@@ -32,7 +32,7 @@ Neo4j Sink 连接器通过执行 Cypher 语句把 SeaTunnel 数据写入 Neo4j
 | kerberos_ticket            | String  | 否    | -          | 用于 Neo4j 认证的 
Kerberos ticket。                                                                
             |
 | database                   | String  | 是    | -          | Neo4j 数据库名。       
                                                                                
         |
 | query                      | String  | 是    | -          | 写入数据使用的 Cypher 
语句。`ONE_BY_ONE` 模式使用 `$name` 这类占位符;`BATCH` 模式使用 `UNWIND $batch AS row`。         
    |
-| queryParamPosition         | Object  | 是    | -          | Cypher 
参数名和输入行字段位置的映射。连接器配置校验要求必须填写。                                                   
            |
+| queryParamPosition         | Object  | 仅 ONE_BY_ONE | -          | Cypher 
参数名和输入行字段位置的映射。`write_mode = "ONE_BY_ONE"` 时必须填写。                               
                |
 | max_batch_size             | Integer | 否    | 500        | `write_mode = 
"BATCH"` 时,单个事务最多写入的数据条数,必须大于 0。                                                
         |
 | write_mode                 | String  | 否    | ONE_BY_ONE | 写入模式。可选值为 
`ONE_BY_ONE` 和 `BATCH`。                                                         
                |
 | max_transaction_retry_time | Long    | 否    | 30         | 最大事务重试时间,单位为秒。    
                                                                                
        |
@@ -41,10 +41,9 @@ Neo4j Sink 连接器通过执行 Cypher 语句把 SeaTunnel 数据写入 Neo4j
 
 ## 注意事项
 
-- 认证方式只选一种:用户名密码、bearer token 或 Kerberos ticket。
+- 至少配置一种认证方式:用户名密码、bearer token 或 Kerberos 
ticket。如果同时配置多种方式,优先级依次为用户名密码、bearer token、Kerberos ticket。
 - `ONE_BY_ONE` 模式下,`queryParamPosition` 用来把 Cypher 占位符映射到输入行的字段位置。
 - `BATCH` 模式下,查询语句应使用 `UNWIND $batch AS row`,连接器会通过 `batch` 变量传入一批数据。
-- `BATCH` 模式虽然从 `row` 中取值,但连接器配置校验仍要求填写 `queryParamPosition`。
 - `queryParamPosition` 中的字段位置从 `0` 开始,顺序对应上游输入表结构。
 - `BATCH` 模式下,每个 `row` 使用上游字段名取值,因此 Cypher 语句里的字段名需要和上游表结构一致。
 
@@ -104,11 +103,6 @@ sink {
     max_transaction_retry_time = 3
     max_connection_timeout = 10
 
-    queryParamPosition = {
-      name = 0
-      age = 1
-    }
-
     query = "UNWIND $batch AS row CREATE (n:BatchLabel) SET n.name = row.name, 
n.age = row.age"
   }
 }
diff --git 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jAuthenticationConditions.java
 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jAuthenticationConditions.java
new file mode 100644
index 0000000000..6a28a4fd2d
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jAuthenticationConditions.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.neo4j.config;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConditionExtension;
+
+public final class Neo4jAuthenticationConditions {
+
+    public static final ConditionExtension<String> AUTHENTICATION_METHOD =
+            new AuthenticationMethodCondition();
+
+    public static final ConditionExtension<String> USERNAME_REQUIRES_PASSWORD =
+            new UsernamePasswordCondition();
+
+    private Neo4jAuthenticationConditions() {}
+
+    private static final class AuthenticationMethodCondition implements 
ConditionExtension<String> {
+
+        @Override
+        public String description() {
+            return "at least one of 'username', 'bearer_token', or 
'kerberos_ticket' must be configured";
+        }
+
+        @Override
+        public boolean evaluate(ReadonlyConfig config, String uri) {
+            return 
config.getOptional(Neo4jBaseOptions.KEY_USERNAME).isPresent()
+                    || 
config.getOptional(Neo4jBaseOptions.KEY_BEARER_TOKEN).isPresent()
+                    || 
config.getOptional(Neo4jBaseOptions.KEY_KERBEROS_TICKET).isPresent();
+        }
+    }
+
+    private static final class UsernamePasswordCondition implements 
ConditionExtension<String> {
+
+        @Override
+        public String description() {
+            return "requires 'password' to be configured when 'username' is 
configured";
+        }
+
+        @Override
+        public boolean evaluate(ReadonlyConfig config, String username) {
+            return 
config.getOptional(Neo4jBaseOptions.KEY_PASSWORD).isPresent();
+        }
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jQueryInfo.java
 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jQueryInfo.java
index f3118d56ca..9e8c76cd3d 100644
--- 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jQueryInfo.java
+++ 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jQueryInfo.java
@@ -19,12 +19,6 @@ package 
org.apache.seatunnel.connectors.seatunnel.neo4j.config;
 
 import org.apache.seatunnel.shade.com.typesafe.config.Config;
 
-import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode;
-import org.apache.seatunnel.common.config.CheckConfigUtil;
-import org.apache.seatunnel.common.config.CheckResult;
-import org.apache.seatunnel.common.constants.PluginType;
-import 
org.apache.seatunnel.connectors.seatunnel.neo4j.exception.Neo4jConnectorException;
-
 import org.neo4j.driver.AuthTokens;
 
 import lombok.Data;
@@ -41,7 +35,6 @@ import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jBaseOp
 import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jBaseOptions.KEY_PASSWORD;
 import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jBaseOptions.KEY_QUERY;
 import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jBaseOptions.KEY_USERNAME;
-import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jBaseOptions.PLUGIN_NAME;
 
 /**
  * Because Neo4jQueryInfo is one of the Neo4jSink's member variable, So 
Neo4jQueryInfo need
@@ -52,50 +45,17 @@ public abstract class Neo4jQueryInfo implements 
Serializable {
     protected DriverBuilder driverBuilder;
     protected String query;
 
-    protected PluginType pluginType;
-
-    public Neo4jQueryInfo(Config config, PluginType pluginType) {
-        this.pluginType = pluginType;
-        this.driverBuilder = prepareDriver(config, pluginType);
-        this.query = prepareQuery(config, pluginType);
+    public Neo4jQueryInfo(Config config) {
+        this.driverBuilder = prepareDriver(config);
+        this.query = prepareQuery(config);
     }
 
-    // which is identical to the prepareDriver methods of the source and sink.
-    // the only difference is the pluginType mentioned in the error messages.
-    // so move code to here
-    protected DriverBuilder prepareDriver(Config config, PluginType 
pluginType) {
-        final CheckResult uriConfigCheck =
-                CheckConfigUtil.checkAllExists(config, KEY_NEO4J_URI.key(), 
KEY_DATABASE.key());
-        final CheckResult authConfigCheck =
-                CheckConfigUtil.checkAtLeastOneExists(
-                        config,
-                        KEY_USERNAME.key(),
-                        KEY_BEARER_TOKEN.key(),
-                        KEY_KERBEROS_TICKET.key());
-        final CheckResult mergedConfigCheck =
-                CheckConfigUtil.mergeCheckResults(uriConfigCheck, 
authConfigCheck);
-        if (!mergedConfigCheck.isSuccess()) {
-            throw new Neo4jConnectorException(
-                    SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
-                    String.format(
-                            "PluginName: %s, PluginType: %s, Message: %s",
-                            PLUGIN_NAME, pluginType, 
mergedConfigCheck.getMsg()));
-        }
-
+    protected DriverBuilder prepareDriver(Config config) {
         final URI uri = URI.create(config.getString(KEY_NEO4J_URI.key()));
 
         final DriverBuilder driverBuilder = DriverBuilder.create(uri);
 
         if (config.hasPath(KEY_USERNAME.key())) {
-            final CheckResult pwParamCheck =
-                    CheckConfigUtil.checkAllExists(config, KEY_PASSWORD.key());
-            if (!pwParamCheck.isSuccess()) {
-                throw new Neo4jConnectorException(
-                        SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
-                        String.format(
-                                "PluginName: %s, PluginType: %s, Message: %s",
-                                PLUGIN_NAME, pluginType, 
pwParamCheck.getMsg()));
-            }
             final String username = config.getString(KEY_USERNAME.key());
             final String password = config.getString(KEY_PASSWORD.key());
 
@@ -125,15 +85,7 @@ public abstract class Neo4jQueryInfo implements 
Serializable {
         return driverBuilder;
     }
 
-    private String prepareQuery(Config config, PluginType pluginType) {
-        CheckResult queryConfigCheck = CheckConfigUtil.checkAllExists(config, 
KEY_QUERY.key());
-        if (!queryConfigCheck.isSuccess()) {
-            throw new Neo4jConnectorException(
-                    SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
-                    String.format(
-                            "PluginName: %s, PluginType: %s, Message: %s",
-                            PLUGIN_NAME, pluginType, 
queryConfigCheck.getMsg()));
-        }
+    private String prepareQuery(Config config) {
         return config.getString(KEY_QUERY.key());
     }
 }
diff --git 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jSinkQueryInfo.java
 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jSinkQueryInfo.java
index 674b96442a..25b50cbd18 100644
--- 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jSinkQueryInfo.java
+++ 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jSinkQueryInfo.java
@@ -19,19 +19,13 @@ package 
org.apache.seatunnel.connectors.seatunnel.neo4j.config;
 
 import org.apache.seatunnel.shade.com.typesafe.config.Config;
 
-import org.apache.seatunnel.api.common.SeaTunnelAPIErrorCode;
-import org.apache.seatunnel.common.config.CheckConfigUtil;
-import org.apache.seatunnel.common.config.CheckResult;
-import org.apache.seatunnel.common.constants.PluginType;
 import org.apache.seatunnel.connectors.seatunnel.neo4j.constants.SinkWriteMode;
-import 
org.apache.seatunnel.connectors.seatunnel.neo4j.exception.Neo4jConnectorException;
 
 import lombok.Getter;
 import lombok.Setter;
 
 import java.util.Map;
 
-import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jBaseOptions.PLUGIN_NAME;
 import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSinkOptions.MAX_BATCH_SIZE;
 import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSinkOptions.QUERY_PARAM_POSITION;
 import static 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSinkOptions.WRITE_MODE;
@@ -50,7 +44,7 @@ public class Neo4jSinkQueryInfo extends Neo4jQueryInfo {
     }
 
     public Neo4jSinkQueryInfo(Config config) {
-        super(config, PluginType.SINK);
+        super(config);
 
         this.writeMode = prepareWriteMode(config);
 
@@ -62,35 +56,14 @@ public class Neo4jSinkQueryInfo extends Neo4jQueryInfo {
     }
 
     private void prepareOneByOneConfig(Config config) {
-
-        CheckResult queryConfigCheck =
-                CheckConfigUtil.checkAllExists(config, 
QUERY_PARAM_POSITION.key());
-
-        if (!queryConfigCheck.isSuccess()) {
-            throw new Neo4jConnectorException(
-                    SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
-                    String.format(
-                            "PluginName: %s, PluginType: %s, Message: %s",
-                            PLUGIN_NAME, PluginType.SINK, 
queryConfigCheck.getMsg()));
-        }
-
         // set queryParamPosition
         this.queryParamPosition = 
config.getObject(QUERY_PARAM_POSITION.key()).unwrapped();
     }
 
     private void prepareBatchWriteConfig(Config config) {
-
         // batch size
         if (config.hasPath(MAX_BATCH_SIZE.key())) {
-            int batchSize = config.getInt(MAX_BATCH_SIZE.key());
-            if (batchSize <= 0) {
-                throw new Neo4jConnectorException(
-                        SeaTunnelAPIErrorCode.CONFIG_VALIDATION_FAILED,
-                        String.format(
-                                "PluginName: %s, PluginType: %s, Message: %s",
-                                PLUGIN_NAME, PluginType.SINK, "maxBatchSize 
must greater than 0"));
-            }
-            this.maxBatchSize = batchSize;
+            this.maxBatchSize = config.getInt(MAX_BATCH_SIZE.key());
         } else {
             this.maxBatchSize = MAX_BATCH_SIZE.defaultValue();
         }
diff --git 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jSourceQueryInfo.java
 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jSourceQueryInfo.java
index cd98f54f96..da3c3f89f6 100644
--- 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jSourceQueryInfo.java
+++ 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/config/Neo4jSourceQueryInfo.java
@@ -19,11 +19,9 @@ package 
org.apache.seatunnel.connectors.seatunnel.neo4j.config;
 
 import org.apache.seatunnel.shade.com.typesafe.config.Config;
 
-import org.apache.seatunnel.common.constants.PluginType;
-
 public class Neo4jSourceQueryInfo extends Neo4jQueryInfo {
 
     public Neo4jSourceQueryInfo(Config pluginConfig) {
-        super(pluginConfig, PluginType.SOURCE);
+        super(pluginConfig);
     }
 }
diff --git 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/sink/Neo4jSinkFactory.java
 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/sink/Neo4jSinkFactory.java
index f2623c32f5..aef47bec3e 100644
--- 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/sink/Neo4jSinkFactory.java
+++ 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/sink/Neo4jSinkFactory.java
@@ -17,13 +17,16 @@
 
 package org.apache.seatunnel.connectors.seatunnel.neo4j.sink;
 
+import org.apache.seatunnel.api.configuration.util.Conditions;
 import org.apache.seatunnel.api.configuration.util.OptionRule;
 import org.apache.seatunnel.api.table.connector.TableSink;
 import org.apache.seatunnel.api.table.factory.Factory;
 import org.apache.seatunnel.api.table.factory.TableSinkFactory;
 import org.apache.seatunnel.api.table.factory.TableSinkFactoryContext;
+import 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jAuthenticationConditions;
 import org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSinkOptions;
 import 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSinkQueryInfo;
+import org.apache.seatunnel.connectors.seatunnel.neo4j.constants.SinkWriteMode;
 
 import com.google.auto.service.AutoService;
 
@@ -39,16 +42,31 @@ public class Neo4jSinkFactory implements TableSinkFactory {
         return OptionRule.builder()
                 .required(
                         Neo4jSinkOptions.KEY_NEO4J_URI,
-                        Neo4jSinkOptions.KEY_DATABASE,
-                        Neo4jSinkOptions.KEY_QUERY,
-                        Neo4jSinkOptions.QUERY_PARAM_POSITION)
+                        Conditions.extension(
+                                Neo4jSinkOptions.KEY_NEO4J_URI,
+                                
Neo4jAuthenticationConditions.AUTHENTICATION_METHOD))
+                .required(Neo4jSinkOptions.KEY_DATABASE, 
Neo4jSinkOptions.KEY_QUERY)
                 .optional(
                         Neo4jSinkOptions.KEY_USERNAME,
+                        Conditions.extension(
+                                Neo4jSinkOptions.KEY_USERNAME,
+                                
Neo4jAuthenticationConditions.USERNAME_REQUIRES_PASSWORD))
+                .optional(
                         Neo4jSinkOptions.KEY_PASSWORD,
                         Neo4jSinkOptions.KEY_BEARER_TOKEN,
                         Neo4jSinkOptions.KEY_KERBEROS_TICKET,
                         Neo4jSinkOptions.KEY_MAX_CONNECTION_TIMEOUT,
-                        Neo4jSinkOptions.KEY_MAX_TRANSACTION_RETRY_TIME)
+                        Neo4jSinkOptions.KEY_MAX_TRANSACTION_RETRY_TIME,
+                        Neo4jSinkOptions.WRITE_MODE,
+                        Neo4jSinkOptions.MAX_BATCH_SIZE)
+                .conditional(
+                        Neo4jSinkOptions.WRITE_MODE,
+                        SinkWriteMode.ONE_BY_ONE,
+                        Neo4jSinkOptions.QUERY_PARAM_POSITION)
+                .conditional(
+                        Neo4jSinkOptions.WRITE_MODE,
+                        SinkWriteMode.BATCH,
+                        
Conditions.greaterThan(Neo4jSinkOptions.MAX_BATCH_SIZE, 0))
                 .build();
     }
 
diff --git 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/source/Neo4jSourceFactory.java
 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/source/Neo4jSourceFactory.java
index eae2e8fc79..b847b0e9a7 100644
--- 
a/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/source/Neo4jSourceFactory.java
+++ 
b/seatunnel-connectors-v2/connector-neo4j/src/main/java/org/apache/seatunnel/connectors/seatunnel/neo4j/source/Neo4jSourceFactory.java
@@ -34,6 +34,7 @@ import org.apache.seatunnel.api.table.connector.TableSource;
 import org.apache.seatunnel.api.table.factory.Factory;
 import org.apache.seatunnel.api.table.factory.TableSourceFactory;
 import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
+import 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jAuthenticationConditions;
 import 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSourceOptions;
 import 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSourceQueryInfo;
 import 
org.apache.seatunnel.connectors.seatunnel.neo4j.exception.Neo4jConnectorException;
@@ -57,7 +58,12 @@ public class Neo4jSourceFactory implements 
TableSourceFactory {
     @Override
     public OptionRule optionRule() {
         return OptionRule.builder()
-                .required(Neo4jSourceOptions.KEY_NEO4J_URI, 
Neo4jSourceOptions.KEY_DATABASE)
+                .required(
+                        Neo4jSourceOptions.KEY_NEO4J_URI,
+                        Conditions.extension(
+                                Neo4jSourceOptions.KEY_NEO4J_URI,
+                                
Neo4jAuthenticationConditions.AUTHENTICATION_METHOD))
+                .required(Neo4jSourceOptions.KEY_DATABASE)
                 .exclusive(Neo4jSourceOptions.KEY_QUERY, 
ConnectorCommonOptions.TABLE_CONFIGS)
                 .optional(
                         Neo4jSourceOptions.KEY_QUERY,
@@ -71,6 +77,10 @@ public class Neo4jSourceFactory implements 
TableSourceFactory {
                                 ConnectorCommonOptions.TABLE_CONFIGS, new 
TableConfigsValidator()))
                 .optional(
                         Neo4jSourceOptions.KEY_USERNAME,
+                        Conditions.extension(
+                                Neo4jSourceOptions.KEY_USERNAME,
+                                
Neo4jAuthenticationConditions.USERNAME_REQUIRES_PASSWORD))
+                .optional(
                         Neo4jSourceOptions.KEY_PASSWORD,
                         Neo4jSourceOptions.KEY_BEARER_TOKEN,
                         Neo4jSourceOptions.KEY_KERBEROS_TICKET,
diff --git 
a/seatunnel-connectors-v2/connector-neo4j/src/test/java/org/apache/seatunnel/connectors/seatunnel/neo4j/Neo4jFactoryTest.java
 
b/seatunnel-connectors-v2/connector-neo4j/src/test/java/org/apache/seatunnel/connectors/seatunnel/neo4j/Neo4jFactoryTest.java
index 4c6220a97f..0550d9bfca 100644
--- 
a/seatunnel-connectors-v2/connector-neo4j/src/test/java/org/apache/seatunnel/connectors/seatunnel/neo4j/Neo4jFactoryTest.java
+++ 
b/seatunnel-connectors-v2/connector-neo4j/src/test/java/org/apache/seatunnel/connectors/seatunnel/neo4j/Neo4jFactoryTest.java
@@ -22,6 +22,9 @@ import 
org.apache.seatunnel.api.configuration.util.ConfigValidator;
 import org.apache.seatunnel.api.configuration.util.OptionValidationException;
 import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
+import org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSinkOptions;
+import 
org.apache.seatunnel.connectors.seatunnel.neo4j.config.Neo4jSourceOptions;
+import org.apache.seatunnel.connectors.seatunnel.neo4j.constants.SinkWriteMode;
 import org.apache.seatunnel.connectors.seatunnel.neo4j.sink.Neo4jSinkFactory;
 import org.apache.seatunnel.connectors.seatunnel.neo4j.source.Neo4jSource;
 import 
org.apache.seatunnel.connectors.seatunnel.neo4j.source.Neo4jSourceFactory;
@@ -143,6 +146,117 @@ class Neo4jFactoryTest {
                                 () -> validateSource(duplicateTable)));
     }
 
+    @Test
+    void sourceAndSinkOptionRulesRequireAuthentication() {
+        Map<String, Object> sourceConfig = validSourceConfig();
+        removeAuthentication(sourceConfig);
+
+        Map<String, Object> sinkConfig = validSinkConfig();
+        removeAuthentication(sinkConfig);
+
+        Assertions.assertAll(
+                () ->
+                        Assertions.assertThrows(
+                                OptionValidationException.class,
+                                () -> validateSource(sourceConfig)),
+                () ->
+                        Assertions.assertThrows(
+                                OptionValidationException.class, () -> 
validateSink(sinkConfig)));
+    }
+
+    @Test
+    void sourceAndSinkOptionRulesRequirePasswordWithUsername() {
+        Map<String, Object> sourceConfig = validSourceConfig();
+        sourceConfig.remove(Neo4jSourceOptions.KEY_PASSWORD.key());
+
+        Map<String, Object> sinkConfig = validSinkConfig();
+        sinkConfig.remove(Neo4jSinkOptions.KEY_PASSWORD.key());
+
+        Assertions.assertAll(
+                () ->
+                        Assertions.assertThrows(
+                                OptionValidationException.class,
+                                () -> validateSource(sourceConfig)),
+                () ->
+                        Assertions.assertThrows(
+                                OptionValidationException.class, () -> 
validateSink(sinkConfig)));
+    }
+
+    @Test
+    void sourceAndSinkOptionRulesAcceptTokenAuthentication() {
+        Map<String, Object> bearerSource = validSourceConfig();
+        useTokenAuthentication(bearerSource, 
Neo4jSourceOptions.KEY_BEARER_TOKEN.key());
+        Map<String, Object> kerberosSource = validSourceConfig();
+        useTokenAuthentication(kerberosSource, 
Neo4jSourceOptions.KEY_KERBEROS_TICKET.key());
+
+        Map<String, Object> bearerSink = validSinkConfig();
+        useTokenAuthentication(bearerSink, 
Neo4jSinkOptions.KEY_BEARER_TOKEN.key());
+        Map<String, Object> kerberosSink = validSinkConfig();
+        useTokenAuthentication(kerberosSink, 
Neo4jSinkOptions.KEY_KERBEROS_TICKET.key());
+
+        Assertions.assertAll(
+                () -> Assertions.assertDoesNotThrow(() -> 
validateSource(bearerSource)),
+                () -> Assertions.assertDoesNotThrow(() -> 
validateSource(kerberosSource)),
+                () -> Assertions.assertDoesNotThrow(() -> 
validateSink(bearerSink)),
+                () -> Assertions.assertDoesNotThrow(() -> 
validateSink(kerberosSink)));
+    }
+
+    @Test
+    void sourceAndSinkOptionRulesPreserveMultipleAuthenticationCompatibility() 
{
+        Map<String, Object> sourceConfig = validSourceConfig();
+        sourceConfig.put(Neo4jSourceOptions.KEY_BEARER_TOKEN.key(), 
"bearer-token");
+
+        Map<String, Object> sinkConfig = validSinkConfig();
+        sinkConfig.put(Neo4jSinkOptions.KEY_KERBEROS_TICKET.key(), 
"kerberos-ticket");
+
+        Assertions.assertAll(
+                () -> Assertions.assertDoesNotThrow(() -> 
validateSource(sourceConfig)),
+                () -> Assertions.assertDoesNotThrow(() -> 
validateSink(sinkConfig)));
+    }
+
+    @Test
+    void sinkOptionRuleRequiresQueryParamPositionInDefaultMode() {
+        Map<String, Object> config = validSinkConfig();
+        config.remove(Neo4jSinkOptions.QUERY_PARAM_POSITION.key());
+
+        Assertions.assertThrows(OptionValidationException.class, () -> 
validateSink(config));
+    }
+
+    @Test
+    void sinkOptionRuleDoesNotRequireQueryParamPositionInBatchMode() {
+        Map<String, Object> config = validSinkConfig();
+        config.put(Neo4jSinkOptions.WRITE_MODE.key(), SinkWriteMode.BATCH);
+        config.remove(Neo4jSinkOptions.QUERY_PARAM_POSITION.key());
+
+        Assertions.assertDoesNotThrow(() -> validateSink(config));
+    }
+
+    @Test
+    void sinkOptionRuleRequiresPositiveBatchSizeOnlyInBatchMode() {
+        Map<String, Object> zeroBatchSize = validSinkConfig();
+        zeroBatchSize.put(Neo4jSinkOptions.WRITE_MODE.key(), 
SinkWriteMode.BATCH);
+        zeroBatchSize.put(Neo4jSinkOptions.MAX_BATCH_SIZE.key(), 0);
+        zeroBatchSize.remove(Neo4jSinkOptions.QUERY_PARAM_POSITION.key());
+
+        Map<String, Object> negativeBatchSize = validSinkConfig();
+        negativeBatchSize.put(Neo4jSinkOptions.WRITE_MODE.key(), 
SinkWriteMode.BATCH);
+        negativeBatchSize.put(Neo4jSinkOptions.MAX_BATCH_SIZE.key(), -1);
+        negativeBatchSize.remove(Neo4jSinkOptions.QUERY_PARAM_POSITION.key());
+
+        Map<String, Object> oneByOne = validSinkConfig();
+        oneByOne.put(Neo4jSinkOptions.MAX_BATCH_SIZE.key(), 0);
+
+        Assertions.assertAll(
+                () ->
+                        Assertions.assertThrows(
+                                OptionValidationException.class, () -> 
validateSink(zeroBatchSize)),
+                () ->
+                        Assertions.assertThrows(
+                                OptionValidationException.class,
+                                () -> validateSink(negativeBatchSize)),
+                () -> Assertions.assertDoesNotThrow(() -> 
validateSink(oneByOne)));
+    }
+
     @Test
     void sourceFactoryProducesCatalogTableForEachQuery() {
         Map<String, Object> config = sourceConnectionConfig();
@@ -177,6 +291,36 @@ class Neo4jFactoryTest {
         return config;
     }
 
+    private static Map<String, Object> validSourceConfig() {
+        Map<String, Object> config = sourceConnectionConfig();
+        config.put(Neo4jSourceOptions.KEY_QUERY.key(), "MATCH (p:Person) 
RETURN p.name");
+        config.put("schema", schema("people"));
+        return config;
+    }
+
+    private static Map<String, Object> validSinkConfig() {
+        Map<String, Object> config = sourceConnectionConfig();
+        config.put(Neo4jSinkOptions.KEY_QUERY.key(), "CREATE (p:Person {name: 
$name, age: $age})");
+
+        Map<String, String> queryParamPosition = new HashMap<>();
+        queryParamPosition.put("name", "0");
+        queryParamPosition.put("age", "1");
+        config.put(Neo4jSinkOptions.QUERY_PARAM_POSITION.key(), 
queryParamPosition);
+        return config;
+    }
+
+    private static void removeAuthentication(Map<String, Object> config) {
+        config.remove(Neo4jSourceOptions.KEY_USERNAME.key());
+        config.remove(Neo4jSourceOptions.KEY_PASSWORD.key());
+        config.remove(Neo4jSourceOptions.KEY_BEARER_TOKEN.key());
+        config.remove(Neo4jSourceOptions.KEY_KERBEROS_TICKET.key());
+    }
+
+    private static void useTokenAuthentication(Map<String, Object> config, 
String tokenKey) {
+        removeAuthentication(config);
+        config.put(tokenKey, "token");
+    }
+
     private static Map<String, Object> tableConfig(String table, String query) 
{
         Map<String, Object> tableConfig = new HashMap<>();
         tableConfig.put("query", query);
@@ -198,4 +342,9 @@ class Neo4jFactoryTest {
         ConfigValidator.of(ReadonlyConfig.fromMap(config))
                 .validate(new Neo4jSourceFactory().optionRule());
     }
+
+    private static void validateSink(Map<String, Object> config) {
+        ConfigValidator.of(ReadonlyConfig.fromMap(config))
+                .validate(new Neo4jSinkFactory().optionRule());
+    }
 }

Reply via email to