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 018e360cbc [Feature][Transform-V2] Add multi-table rule match mode 
(#11099)
018e360cbc is described below

commit 018e360cbc0992671629ce10b4dd604693433b5f
Author: Goutam Adwant <[email protected]>
AuthorDate: Sun Jul 5 01:10:53 2026 -0700

    [Feature][Transform-V2] Add multi-table rule match mode (#11099)
    
    Signed-off-by: goutamadwant <[email protected]>
    Co-authored-by: DanielLeens <[email protected]>
---
 docs/en/transforms/transform-multi-table.md        |  35 +-
 docs/zh/transforms/transform-multi-table.md        |  33 +-
 .../adaptsink/DefineSinkTypeTransformFactory.java  |   1 +
 .../AbstractMultiCatalogFlatMapTransform.java      |  10 +
 .../common/AbstractMultiCatalogMapTransform.java   |  10 +
 .../common/AbstractMultiCatalogTransform.java      | 102 ++++-
 .../transform/common/ChainedFlatMapTransform.java  | 116 +++++
 .../transform/common/ChainedMapTransform.java      | 110 +++++
 .../transform/common/TransformCommonOptions.java   |  11 +
 .../transform/copy/CopyFieldTransformFactory.java  |   1 +
 .../DynamicCompileTransformFactory.java            |   1 +
 .../encrypt/FieldEncryptTransformFactory.java      |   1 +
 .../fieldmapper/FieldMapperTransformFactory.java   |   1 +
 .../filter/FilterFieldTransformFactory.java        |   1 +
 .../FilterRowKindTransformFactory.java             |   1 +
 .../jsonpath/JsonPathTransformFactory.java         |   1 +
 .../metadata/MetadataTransformFactory.java         |   1 +
 .../embedding/EmbeddingTransformFactory.java       |   1 +
 .../nlpmodel/llm/LLMTransformFactory.java          |   1 +
 .../regexextract/RegexExtractTransformFactory.java |   3 +-
 .../rename/FieldRenameTransformFactory.java        |   1 +
 .../rename/TableRenameTransformFactory.java        |   1 +
 .../transform/replace/ReplaceTransformFactory.java |   1 +
 .../rowkind/RowKindExtractorTransformFactory.java  |   1 +
 .../transform/split/SplitTransformFactory.java     |   1 +
 .../transform/sql/SQLTransformFactory.java         |   1 +
 .../table/TableFilterTransformFactory.java         |   1 +
 .../table/TableMergeTransformFactory.java          |   1 +
 .../validator/DataValidatorTransformFactory.java   |   1 +
 .../common/AbstractMultiCatalogTransformTest.java  | 476 +++++++++++++++++++++
 .../copy/CopyFieldMultiCatalogTransformTest.java   |  94 ++++
 31 files changed, 998 insertions(+), 22 deletions(-)

diff --git a/docs/en/transforms/transform-multi-table.md 
b/docs/en/transforms/transform-multi-table.md
index 2e14f801f0..3097000bb8 100644
--- a/docs/en/transforms/transform-multi-table.md
+++ b/docs/en/transforms/transform-multi-table.md
@@ -18,7 +18,8 @@ Multi-table Transform has no limitations on Transform 
capabilities; any Transfor
 
|----------------------------|--------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | table_match_regex          | String | No       | .*      | A regular 
expression to match the tables that require transformation. By default, it 
matches all tables. Note that this table name refers to the actual upstream 
table name, not `plugin_output`.                                                
               |
 | table_transform            | List   | No       | -       | You can use a 
list in `table_transform` to specify rules for individual tables. If a 
transformation rule is configured for a specific table in `table_transform`, 
the outer rules will not apply to that table. The rules in `table_transform` 
take precedence. |
-| table_transform.table_path | String | No       | -       | When configuring 
a transformation rule for a table in `table_transform`, you need to specify the 
table path using the `table_path` field. The table path should include 
`databaseName[.schemaName].tableName`.                                          
        |
+| table_transform.table_path | String | No       | -       | When configuring 
a transformation rule for a table in `table_transform`, you need to specify the 
table path using the `table_path` field. The table path should include 
`databaseName[.schemaName].tableName` and is matched exactly.                   
      |
+| rule_match_mode            | String | No       | -       | Controls how 
multiple `table_transform` entries with the same exact `table_path` are 
evaluated. Supported values are `FIRST_MATCH` and `ALL_MATCH`.                  
                                                                                
       |
 
 ## Matching Logic
 
@@ -66,6 +67,36 @@ This allows us to handle transformations for multiple tables 
within a single tra
 
 For each table, the priority of configuration is: `table_transform` > 
`table_match_regex`. If no rules match a table, no transformation will be 
applied.
 
+`table_transform.table_path` uses exact table path matching. `rule_match_mode` 
only controls the case where multiple `table_transform` entries use the same 
exact `table_path`:
+
+- If `rule_match_mode` is not configured, duplicate exact `table_path` entries 
are rejected during config resolution.
+- `FIRST_MATCH`: apply the first matching `table_transform` entry in 
declaration order.
+- `ALL_MATCH`: apply all matching `table_transform` entries in declaration 
order. The output of an earlier matching rule becomes the input of the next 
matching rule for the same table.
+
+For example, when `rule_match_mode` is `ALL_MATCH`, the following 
configuration first copies `name` to `name2` for `test.xyz`, and then copies 
`name2` to `name3` for the same table:
+
+```hocon
+transform {
+  Copy {
+    rule_match_mode = "ALL_MATCH"
+
+    table_transform = [{
+      table_path = "test.xyz"
+      src_field = "name"
+      dest_field = "name2"
+    }, {
+      table_path = "test.xyz"
+      src_field = "name2"
+      dest_field = "name3"
+    }]
+  }
+}
+```
+
+Output structure:
+
+| id | name | age | name2 | name3 |
+
 Below are the transform configurations for each table:
 
 - **test.abc** and **test.abcd**
@@ -125,4 +156,4 @@ Output structure:
 
 | id | name | age |
 
-In this example, we used the Copy transform, but all transforms in SeaTunnel 
support multi-table transformations, and you can configure them similarly 
within the corresponding transform block.
\ No newline at end of file
+In this example, we used the Copy transform, but all transforms in SeaTunnel 
support multi-table transformations, and you can configure them similarly 
within the corresponding transform block.
diff --git a/docs/zh/transforms/transform-multi-table.md 
b/docs/zh/transforms/transform-multi-table.md
index 8fa346a422..ae0f08882b 100644
--- a/docs/zh/transforms/transform-multi-table.md
+++ b/docs/zh/transforms/transform-multi-table.md
@@ -20,7 +20,8 @@ SeaTunnel 的 Multi-Table Transform 支持在上游插件输出多个表时,
 
|----------------------------|--------|----------|---------|--------------------------------------------------------------------------------------------------|
 | table_match_regex          | String | No       | .*      | 
表名的正则表达式,通过正则表达式来匹配需要进行转换的表,默认匹配所有的表。注意这个表名是上游的真正表名,不是`plugin_output`。          
                 |
 | table_transform            | List   | No       | -       | 
可以通过table_transform列表来指定部分表的规则,当在table_transform中配置某个表的转换规则后,外层针对当前表的规则不会生效,以table_transform中的为准
 |
-| table_transform.table_path | String | No       | -       | 
当在table_transform中配置某个表的转换规则后,需要使用table_path字段指定表名,表名需要包含`databaseName[.schemaName].tableName`。
  |
+| table_transform.table_path | String | No       | -       | 
当在table_transform中配置某个表的转换规则后,需要使用table_path字段指定表名,表名需要包含`databaseName[.schemaName].tableName`,并且按完整表路径精确匹配。
  |
+| rule_match_mode            | String | No       | -       | 控制多个 
`table_transform` 配置使用相同精确 `table_path` 时的执行方式,支持 `FIRST_MATCH` 和 `ALL_MATCH`。 |
 
 ## 匹配逻辑
 
@@ -62,6 +63,36 @@ transform {
 
 对于每个表来说,配置的优先级是:`table_transform` > 
`table_match_regex`。如果所有的规则都没有匹配到,那么该表将不会进行任何转换操作。
 
+`table_transform.table_path` 使用精确表路径匹配。`rule_match_mode` 只控制多个 
`table_transform` 配置使用相同精确 `table_path` 的场景:
+
+- 如果没有配置 `rule_match_mode`,重复的精确 `table_path` 配置会在配置解析阶段失败。
+- `FIRST_MATCH`:按声明顺序使用第一个匹配的 `table_transform` 配置。
+- `ALL_MATCH`:按声明顺序执行所有匹配的 `table_transform` 配置。前一个规则的输出会作为同一张表上后一个规则的输入。
+
+例如,当 `rule_match_mode` 为 `ALL_MATCH` 时,下面的配置会先为 `test.xyz` 将 `name` 复制为 
`name2`,然后继续将 `name2` 复制为 `name3`:
+
+```hocon
+transform {
+  Copy {
+    rule_match_mode = "ALL_MATCH"
+
+    table_transform = [{
+      table_path = "test.xyz"
+      src_field = "name"
+      dest_field = "name2"
+    }, {
+      table_path = "test.xyz"
+      src_field = "name2"
+      dest_field = "name3"
+    }]
+  }
+}
+```
+
+输出表结构:
+
+| id | name | age | name2 | name3 |
+
 针对每个表来说,他们的Transform配置是:
 
 - **test.abc**和**test.abcd**
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactory.java
index 2f0d986b22..bbdb6d4650 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/adaptsink/DefineSinkTypeTransformFactory.java
@@ -54,6 +54,7 @@ public class DefineSinkTypeTransformFactory implements 
TableTransformFactory {
                                                 new 
ColumnsStructureValidator())))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogFlatMapTransform.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogFlatMapTransform.java
index b1deb453d6..3e89ae62c6 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogFlatMapTransform.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogFlatMapTransform.java
@@ -21,6 +21,7 @@ import org.apache.seatunnel.api.configuration.ReadonlyConfig;
 import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.api.transform.SeaTunnelFlatMapTransform;
+import org.apache.seatunnel.api.transform.SeaTunnelTransform;
 
 import java.util.List;
 
@@ -33,6 +34,15 @@ public abstract class AbstractMultiCatalogFlatMapTransform 
extends AbstractMulti
         super(inputCatalogTables, config);
     }
 
+    @Override
+    protected SeaTunnelTransform<SeaTunnelRow> composeTransforms(
+            List<SeaTunnelTransform<SeaTunnelRow>> transforms) {
+        if (transforms.size() == 1) {
+            return transforms.get(0);
+        }
+        return new ChainedFlatMapTransform(transforms);
+    }
+
     @Override
     public List<SeaTunnelRow> flatMap(SeaTunnelRow row) {
         if (transformMap.size() == 1) {
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogMapTransform.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogMapTransform.java
index 9e7d184c09..463803d366 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogMapTransform.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogMapTransform.java
@@ -21,6 +21,7 @@ import org.apache.seatunnel.api.configuration.ReadonlyConfig;
 import org.apache.seatunnel.api.table.catalog.CatalogTable;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.api.transform.SeaTunnelMapTransform;
+import org.apache.seatunnel.api.transform.SeaTunnelTransform;
 
 import java.util.List;
 
@@ -33,6 +34,15 @@ public abstract class AbstractMultiCatalogMapTransform 
extends AbstractMultiCata
         super(inputCatalogTables, config);
     }
 
+    @Override
+    protected SeaTunnelTransform<SeaTunnelRow> composeTransforms(
+            List<SeaTunnelTransform<SeaTunnelRow>> transforms) {
+        if (transforms.size() == 1) {
+            return transforms.get(0);
+        }
+        return new ChainedMapTransform(transforms);
+    }
+
     @Override
     public SeaTunnelRow map(SeaTunnelRow row) {
         if (transformMap.size() == 1) {
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransform.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransform.java
index 0d3a5365c6..f3806109a7 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransform.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransform.java
@@ -25,10 +25,11 @@ import 
org.apache.seatunnel.api.table.type.SeaTunnelDataType;
 import org.apache.seatunnel.api.table.type.SeaTunnelRow;
 import org.apache.seatunnel.api.transform.SeaTunnelTransform;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Function;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -50,28 +51,42 @@ public abstract class AbstractMultiCatalogTransform 
implements SeaTunnelTransfor
         this.transformMap = new HashMap<>();
         Pattern tableMatchRegex =
                 
Pattern.compile(config.get(TransformCommonOptions.TABLE_MATCH_REGEX));
-        Map<String, ReadonlyConfig> singleTableConfig =
+        boolean ruleMatchModeConfigured =
+                
config.getOptional(TransformCommonOptions.RULE_MATCH_MODE).isPresent();
+        TransformCommonOptions.RuleMatchMode ruleMatchMode =
+                config.get(TransformCommonOptions.RULE_MATCH_MODE);
+        List<ReadonlyConfig> singleTableConfigs =
                 config.get(TransformCommonOptions.MULTI_TABLES).stream()
                         .map(ReadonlyConfig::fromMap)
                         .filter(c -> c.get(TransformCommonOptions.TABLE_PATH) 
!= null)
-                        .collect(
-                                Collectors.toMap(
-                                        c -> 
c.get(TransformCommonOptions.TABLE_PATH),
-                                        Function.identity()));
+                        .collect(Collectors.toList());
+        if (!ruleMatchModeConfigured) {
+            rejectDuplicateTablePaths(singleTableConfigs);
+        }
 
         inputCatalogTables.forEach(
                 inputCatalogTable -> {
                     String tableId = 
inputCatalogTable.getTableId().toTablePath().toString();
-                    ReadonlyConfig tableConfig;
-                    if (singleTableConfig.containsKey(tableId)) {
-                        tableConfig = singleTableConfig.get(tableId);
+                    List<ReadonlyConfig> tableConfigs =
+                            singleTableConfigs.stream()
+                                    .filter(
+                                            c ->
+                                                    tableId.equals(
+                                                            c.get(
+                                                                    
TransformCommonOptions
+                                                                            
.TABLE_PATH)))
+                                    .collect(Collectors.toList());
+                    if (!tableConfigs.isEmpty()) {
+                        transformMap.put(
+                                tableId,
+                                buildTransform(
+                                        inputCatalogTable,
+                                        selectTableConfigs(tableConfigs, 
ruleMatchMode)));
                     } else if (tableMatchRegex.matcher(tableId).matches()) {
-                        tableConfig = config;
-                    } else {
-                        tableConfig = null;
-                    }
-                    if (tableConfig != null) {
-                        transformMap.put(tableId, 
buildTransform(inputCatalogTable, tableConfig));
+                        transformMap.put(
+                                tableId,
+                                buildTransform(
+                                        inputCatalogTable, 
Collections.singletonList(config)));
                     } else {
                         transformMap.put(tableId, 
createIdentityTransform(inputCatalogTable));
                     }
@@ -91,6 +106,49 @@ public abstract class AbstractMultiCatalogTransform 
implements SeaTunnelTransfor
     protected abstract SeaTunnelTransform<SeaTunnelRow> buildTransform(
             CatalogTable inputCatalogTable, ReadonlyConfig config);
 
+    private void rejectDuplicateTablePaths(List<ReadonlyConfig> tableConfigs) {
+        Map<String, Boolean> tablePaths = new HashMap<>();
+        for (ReadonlyConfig tableConfig : tableConfigs) {
+            String tablePath = 
tableConfig.get(TransformCommonOptions.TABLE_PATH);
+            if (tablePaths.put(tablePath, true) != null) {
+                throw new IllegalStateException(
+                        String.format(
+                                "Duplicate table_transform rules are 
configured for table_path [%s]",
+                                tablePath));
+            }
+        }
+    }
+
+    private List<ReadonlyConfig> selectTableConfigs(
+            List<ReadonlyConfig> tableConfigs, 
TransformCommonOptions.RuleMatchMode ruleMatchMode) {
+        if (ruleMatchMode == TransformCommonOptions.RuleMatchMode.FIRST_MATCH) 
{
+            return Collections.singletonList(tableConfigs.get(0));
+        }
+        return tableConfigs;
+    }
+
+    private SeaTunnelTransform<SeaTunnelRow> buildTransform(
+            CatalogTable inputCatalogTable, List<ReadonlyConfig> configs) {
+        List<SeaTunnelTransform<SeaTunnelRow>> transforms = new ArrayList<>();
+        CatalogTable currentCatalogTable = inputCatalogTable;
+        for (ReadonlyConfig config : configs) {
+            SeaTunnelTransform<SeaTunnelRow> transform =
+                    buildTransform(currentCatalogTable, config);
+            transforms.add(transform);
+            currentCatalogTable = transform.getProducedCatalogTable();
+        }
+        return composeTransforms(transforms);
+    }
+
+    protected SeaTunnelTransform<SeaTunnelRow> composeTransforms(
+            List<SeaTunnelTransform<SeaTunnelRow>> transforms) {
+        if (transforms.size() == 1) {
+            return transforms.get(0);
+        }
+        throw new UnsupportedOperationException(
+                String.format("%s does not support chained transforms", 
getPluginName()));
+    }
+
     protected abstract SeaTunnelTransform<SeaTunnelRow> 
createIdentityTransform(
             CatalogTable catalogTable);
 
@@ -118,18 +176,24 @@ public abstract class AbstractMultiCatalogTransform 
implements SeaTunnelTransfor
         String targetTableId = event.tablePath().toString();
         SeaTunnelTransform<SeaTunnelRow> targetTransform = 
transformMap.get(targetTableId);
         if (targetTransform != null) {
-            targetTransform.mapSchemaChangeEvent(event);
+            SchemaChangeEvent mappedEvent = 
targetTransform.mapSchemaChangeEvent(event);
+            if (mappedEvent == null) {
+                return null;
+            }
             refreshOutputCatalogTables();
             // Propagate this transform's actual produced catalog through the 
chain via the event's
             // changeAfter field (existing API designed for exactly this). 
Downstream transforms
             // and the sink read changeAfter to adopt upstream's actual row 
layout instead of
             // re-applying ALTER locally — which would diverge from the actual 
row order.
-            if (event instanceof AlterTableEvent) {
+            if (mappedEvent instanceof AlterTableEvent) {
                 outputCatalogTables.stream()
                         .filter(t -> 
t.getTableId().toTablePath().toString().equals(targetTableId))
                         .findFirst()
-                        .ifPresent(produced -> ((AlterTableEvent) 
event).setChangeAfter(produced));
+                        .ifPresent(
+                                produced ->
+                                        ((AlterTableEvent) 
mappedEvent).setChangeAfter(produced));
             }
+            return mappedEvent;
         }
         return event;
     }
@@ -151,6 +215,8 @@ public abstract class AbstractMultiCatalogTransform 
implements SeaTunnelTransfor
             SeaTunnelTransform<SeaTunnelRow> inner = transformMap.get(tableId);
             if (inner instanceof AbstractSeaTunnelTransform) {
                 ((AbstractSeaTunnelTransform<?, ?>) 
inner).setInputCatalogTable(newInput);
+            } else if (inner != null) {
+                
inner.setInputCatalogTables(Collections.singletonList(newInput));
             }
         }
         refreshOutputCatalogTables();
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedFlatMapTransform.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedFlatMapTransform.java
new file mode 100644
index 0000000000..28e5895240
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedFlatMapTransform.java
@@ -0,0 +1,116 @@
+/*
+ * 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.transform.common;
+
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.schema.event.SchemaChangeEvent;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.transform.SeaTunnelFlatMapTransform;
+import org.apache.seatunnel.api.transform.SeaTunnelTransform;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class ChainedFlatMapTransform implements 
SeaTunnelFlatMapTransform<SeaTunnelRow> {
+
+    private final List<SeaTunnelTransform<SeaTunnelRow>> transforms;
+
+    ChainedFlatMapTransform(List<SeaTunnelTransform<SeaTunnelRow>> transforms) 
{
+        this.transforms = transforms;
+    }
+
+    @Override
+    public String getPluginName() {
+        return transforms.get(0).getPluginName();
+    }
+
+    @Override
+    public void open() {
+        transforms.forEach(SeaTunnelTransform::open);
+    }
+
+    @Override
+    public List<SeaTunnelRow> flatMap(SeaTunnelRow row) {
+        List<SeaTunnelRow> currentRows = Collections.singletonList(row);
+        for (SeaTunnelTransform<SeaTunnelRow> transform : transforms) {
+            List<SeaTunnelRow> nextRows = new ArrayList<>();
+            for (SeaTunnelRow currentRow : currentRows) {
+                List<SeaTunnelRow> rows =
+                        ((SeaTunnelFlatMapTransform<SeaTunnelRow>) 
transform).flatMap(currentRow);
+                if (rows != null) {
+                    nextRows.addAll(rows);
+                }
+            }
+            currentRows = nextRows;
+        }
+        return currentRows;
+    }
+
+    @Override
+    public CatalogTable getProducedCatalogTable() {
+        return transforms.get(transforms.size() - 1).getProducedCatalogTable();
+    }
+
+    @Override
+    public List<CatalogTable> getProducedCatalogTables() {
+        return Collections.singletonList(getProducedCatalogTable());
+    }
+
+    @Override
+    public SchemaChangeEvent mapSchemaChangeEvent(SchemaChangeEvent 
schemaChangeEvent) {
+        SchemaChangeEvent currentEvent = schemaChangeEvent;
+        for (int i = 0; i < transforms.size(); i++) {
+            SeaTunnelTransform<SeaTunnelRow> transform = transforms.get(i);
+            currentEvent = transform.mapSchemaChangeEvent(currentEvent);
+            if (currentEvent == null) {
+                return null;
+            }
+            if (i + 1 < transforms.size()) {
+                setTransformInput(transforms.get(i + 1), 
transform.getProducedCatalogTable());
+            }
+        }
+        return currentEvent;
+    }
+
+    @Override
+    public void setInputCatalogTables(List<CatalogTable> inputCatalogTables) {
+        if (inputCatalogTables == null || inputCatalogTables.isEmpty()) {
+            return;
+        }
+        CatalogTable currentCatalogTable = inputCatalogTables.get(0);
+        for (SeaTunnelTransform<SeaTunnelRow> transform : transforms) {
+            setTransformInput(transform, currentCatalogTable);
+            currentCatalogTable = transform.getProducedCatalogTable();
+        }
+    }
+
+    @Override
+    public void close() {
+        transforms.forEach(SeaTunnelTransform::close);
+    }
+
+    private void setTransformInput(
+            SeaTunnelTransform<SeaTunnelRow> transform, CatalogTable 
catalogTable) {
+        if (transform instanceof AbstractSeaTunnelTransform) {
+            ((AbstractSeaTunnelTransform<?, ?>) 
transform).setInputCatalogTable(catalogTable);
+        } else {
+            
transform.setInputCatalogTables(Collections.singletonList(catalogTable));
+        }
+    }
+}
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedMapTransform.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedMapTransform.java
new file mode 100644
index 0000000000..26750f869d
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/ChainedMapTransform.java
@@ -0,0 +1,110 @@
+/*
+ * 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.transform.common;
+
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.schema.event.SchemaChangeEvent;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.transform.SeaTunnelMapTransform;
+import org.apache.seatunnel.api.transform.SeaTunnelTransform;
+
+import java.util.Collections;
+import java.util.List;
+
+class ChainedMapTransform implements SeaTunnelMapTransform<SeaTunnelRow> {
+
+    private final List<SeaTunnelTransform<SeaTunnelRow>> transforms;
+
+    ChainedMapTransform(List<SeaTunnelTransform<SeaTunnelRow>> transforms) {
+        this.transforms = transforms;
+    }
+
+    @Override
+    public String getPluginName() {
+        return transforms.get(0).getPluginName();
+    }
+
+    @Override
+    public void open() {
+        transforms.forEach(SeaTunnelTransform::open);
+    }
+
+    @Override
+    public SeaTunnelRow map(SeaTunnelRow row) {
+        SeaTunnelRow currentRow = row;
+        for (SeaTunnelTransform<SeaTunnelRow> transform : transforms) {
+            if (currentRow == null) {
+                return null;
+            }
+            currentRow = ((SeaTunnelMapTransform<SeaTunnelRow>) 
transform).map(currentRow);
+        }
+        return currentRow;
+    }
+
+    @Override
+    public CatalogTable getProducedCatalogTable() {
+        return transforms.get(transforms.size() - 1).getProducedCatalogTable();
+    }
+
+    @Override
+    public List<CatalogTable> getProducedCatalogTables() {
+        return Collections.singletonList(getProducedCatalogTable());
+    }
+
+    @Override
+    public SchemaChangeEvent mapSchemaChangeEvent(SchemaChangeEvent 
schemaChangeEvent) {
+        SchemaChangeEvent currentEvent = schemaChangeEvent;
+        for (int i = 0; i < transforms.size(); i++) {
+            SeaTunnelTransform<SeaTunnelRow> transform = transforms.get(i);
+            currentEvent = transform.mapSchemaChangeEvent(currentEvent);
+            if (currentEvent == null) {
+                return null;
+            }
+            if (i + 1 < transforms.size()) {
+                setTransformInput(transforms.get(i + 1), 
transform.getProducedCatalogTable());
+            }
+        }
+        return currentEvent;
+    }
+
+    @Override
+    public void setInputCatalogTables(List<CatalogTable> inputCatalogTables) {
+        if (inputCatalogTables == null || inputCatalogTables.isEmpty()) {
+            return;
+        }
+        CatalogTable currentCatalogTable = inputCatalogTables.get(0);
+        for (SeaTunnelTransform<SeaTunnelRow> transform : transforms) {
+            setTransformInput(transform, currentCatalogTable);
+            currentCatalogTable = transform.getProducedCatalogTable();
+        }
+    }
+
+    @Override
+    public void close() {
+        transforms.forEach(SeaTunnelTransform::close);
+    }
+
+    private void setTransformInput(
+            SeaTunnelTransform<SeaTunnelRow> transform, CatalogTable 
catalogTable) {
+        if (transform instanceof AbstractSeaTunnelTransform) {
+            ((AbstractSeaTunnelTransform<?, ?>) 
transform).setInputCatalogTable(catalogTable);
+        } else {
+            
transform.setInputCatalogTables(Collections.singletonList(catalogTable));
+        }
+    }
+}
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/TransformCommonOptions.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/TransformCommonOptions.java
index daafc08e2d..268bf1cef9 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/TransformCommonOptions.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/common/TransformCommonOptions.java
@@ -47,6 +47,12 @@ public class TransformCommonOptions {
                     .defaultValue(".*")
                     .withDescription("The regex to match the table path");
 
+    public static final Option<RuleMatchMode> RULE_MATCH_MODE =
+            Options.key("rule_match_mode")
+                    .enumType(RuleMatchMode.class)
+                    .defaultValue(RuleMatchMode.FIRST_MATCH)
+                    .withDescription("The rule match mode for table transform 
config");
+
     public static final Option<ErrorHandleWay> ROW_ERROR_HANDLE_WAY_OPTION =
             Options.key("row_error_handle_way")
                     .singleChoice(
@@ -76,4 +82,9 @@ public class TransformCommonOptions {
                     .noDefaultValue()
                     .withDescription(
                             "Target table name for routing invalid data when 
error_handle_way is ROUTE_TO_TABLE");
+
+    public enum RuleMatchMode {
+        FIRST_MATCH,
+        ALL_MATCH
+    }
 }
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactory.java
index 5de39766ff..4d69bf2ca2 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/copy/CopyFieldTransformFactory.java
@@ -50,6 +50,7 @@ public class CopyFieldTransformFactory implements 
TableTransformFactory {
                 .optional(CopyTransformConfig.DEST_FIELD)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactory.java
index 02141fd0a9..a79fd7104a 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/dynamiccompile/DynamicCompileTransformFactory.java
@@ -57,6 +57,7 @@ public class DynamicCompileTransformFactory implements 
TableTransformFactory {
                         
Conditions.notBlank(DynamicCompileTransformConfig.ABSOLUTE_PATH))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactory.java
index 3c33a095ff..9544b65803 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/encrypt/FieldEncryptTransformFactory.java
@@ -52,6 +52,7 @@ public class FieldEncryptTransformFactory implements 
TableTransformFactory {
                         
Conditions.greaterThan(FieldEncryptTransformConfig.MAX_FIELD_LENGTH, 0))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactory.java
index 8edb4419e3..f9e0f15852 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/fieldmapper/FieldMapperTransformFactory.java
@@ -43,6 +43,7 @@ public class FieldMapperTransformFactory implements 
TableTransformFactory {
                         
Conditions.mapNotEmpty(FieldMapperTransformConfig.FIELD_MAPPER))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactory.java
index bdc273ed27..7860a50dee 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filter/FilterFieldTransformFactory.java
@@ -50,6 +50,7 @@ public class FilterFieldTransformFactory implements 
TableTransformFactory {
                         
Conditions.notEmpty(FilterFieldTransformConfig.EXCLUDE_FIELDS))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filterrowkind/FilterRowKindTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filterrowkind/FilterRowKindTransformFactory.java
index 80dea1693f..5023f37e60 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filterrowkind/FilterRowKindTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/filterrowkind/FilterRowKindTransformFactory.java
@@ -41,6 +41,7 @@ public class FilterRowKindTransformFactory implements 
TableTransformFactory {
                         FilterRowKinkTransformConfig.INCLUDE_KINDS)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactory.java
index d0411fddb3..59b4d36d00 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/jsonpath/JsonPathTransformFactory.java
@@ -52,6 +52,7 @@ public class JsonPathTransformFactory implements 
TableTransformFactory {
                                                 new ColumnsValidator())))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .optional(TransformCommonOptions.ROW_ERROR_HANDLE_WAY_OPTION)
                 .build();
     }
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactory.java
index 479376b888..2988d7ae38 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/metadata/MetadataTransformFactory.java
@@ -42,6 +42,7 @@ public class MetadataTransformFactory implements 
TableTransformFactory {
                         
Conditions.mapNotEmpty(MetadataTransformConfig.METADATA_FIELDS))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/EmbeddingTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/EmbeddingTransformFactory.java
index f75a1eb430..95cc465b73 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/EmbeddingTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/embedding/EmbeddingTransformFactory.java
@@ -81,6 +81,7 @@ public class EmbeddingTransformFactory implements 
TableTransformFactory {
                         EmbeddingTransformConfig.DIMENSION)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformFactory.java
index e703bc9457..67f855b4c6 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/nlpmodel/llm/LLMTransformFactory.java
@@ -66,6 +66,7 @@ public class LLMTransformFactory implements 
TableTransformFactory {
                         LLMTransformConfig.CustomRequestConfig.CUSTOM_CONFIG)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactory.java
index 0502bd27fc..5eee24a304 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/regexextract/RegexExtractTransformFactory.java
@@ -52,7 +52,8 @@ public class RegexExtractTransformFactory implements 
TableTransformFactory {
                         Conditions.extension(
                                 RegexExtractTransformConfig.KEY_DEFAULT_VALUES,
                                 new DefaultValuesLengthValidator()))
-                .optional(TransformCommonOptions.MULTI_TABLES)
+                .optional(
+                        TransformCommonOptions.MULTI_TABLES, 
TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rename/FieldRenameTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rename/FieldRenameTransformFactory.java
index 299bbb5255..78200dfa67 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rename/FieldRenameTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rename/FieldRenameTransformFactory.java
@@ -46,6 +46,7 @@ public class FieldRenameTransformFactory implements 
TableTransformFactory {
                 .optional(CONVERT_CASE, PREFIX, SUFFIX, 
REPLACEMENTS_WITH_REGEX, SPECIFIC)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rename/TableRenameTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rename/TableRenameTransformFactory.java
index 840bfff209..a581849fa9 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rename/TableRenameTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rename/TableRenameTransformFactory.java
@@ -44,6 +44,7 @@ public class TableRenameTransformFactory implements 
TableTransformFactory {
                 .optional(CONVERT_CASE, PREFIX, SUFFIX, 
REPLACEMENTS_WITH_REGEX)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactory.java
index c7c7e1ff64..5e3c32b1ed 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransformFactory.java
@@ -46,6 +46,7 @@ public class ReplaceTransformFactory implements 
TableTransformFactory {
                 .optional(ReplaceTransformConfig.KEY_REPLACE_FIRST)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rowkind/RowKindExtractorTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rowkind/RowKindExtractorTransformFactory.java
index 99011726fc..9837c1b019 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rowkind/RowKindExtractorTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/rowkind/RowKindExtractorTransformFactory.java
@@ -40,6 +40,7 @@ public class RowKindExtractorTransformFactory implements 
TableTransformFactory {
                 .optional(RowKindExtractorTransformConfig.TRANSFORM_TYPE)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/split/SplitTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/split/SplitTransformFactory.java
index d5385a4f59..b65e5fbefd 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/split/SplitTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/split/SplitTransformFactory.java
@@ -44,6 +44,7 @@ public class SplitTransformFactory implements 
TableTransformFactory {
                         
Conditions.notEmpty(SplitTransformConfig.KEY_OUTPUT_FIELDS))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/SQLTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/SQLTransformFactory.java
index 4a3b6db9c5..c1ddda2af7 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/SQLTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/sql/SQLTransformFactory.java
@@ -41,6 +41,7 @@ public class SQLTransformFactory implements 
TableTransformFactory {
                 .required(KEY_QUERY)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterTransformFactory.java
index 701d9f5697..cde7685f86 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableFilterTransformFactory.java
@@ -56,6 +56,7 @@ public class TableFilterTransformFactory implements 
TableTransformFactory {
                 .optional(TableFilterConfig.PATTERN_MODE)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableMergeTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableMergeTransformFactory.java
index 6bf876a235..45bfde346c 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableMergeTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/table/TableMergeTransformFactory.java
@@ -41,6 +41,7 @@ public class TableMergeTransformFactory implements 
TableTransformFactory {
                 .optional(TableMergeConfig.DATABASE, TableMergeConfig.SCHEMA)
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .build();
     }
 
diff --git 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactory.java
 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactory.java
index b391d8339a..e633bfa8db 100644
--- 
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactory.java
+++ 
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/validator/DataValidatorTransformFactory.java
@@ -53,6 +53,7 @@ public class DataValidatorTransformFactory implements 
TableTransformFactory {
                                 .and(Conditions.extension(FIELD_RULES, new 
FieldRulesValidator())))
                 .optional(TransformCommonOptions.MULTI_TABLES)
                 .optional(TransformCommonOptions.TABLE_MATCH_REGEX)
+                .optional(TransformCommonOptions.RULE_MATCH_MODE)
                 .optional(TransformCommonOptions.ROW_ERROR_HANDLE_WAY_OPTION)
                 .optional(TransformCommonOptions.ERROR_TABLE_OPTION)
                 .build();
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransformTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransformTest.java
new file mode 100644
index 0000000000..5dc403dc70
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/common/AbstractMultiCatalogTransformTest.java
@@ -0,0 +1,476 @@
+/*
+ * 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.transform.common;
+
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.Options;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.catalog.Column;
+import org.apache.seatunnel.api.table.catalog.PhysicalColumn;
+import org.apache.seatunnel.api.table.catalog.TableIdentifier;
+import org.apache.seatunnel.api.table.catalog.TableSchema;
+import org.apache.seatunnel.api.table.schema.event.AlterTableAddColumnEvent;
+import org.apache.seatunnel.api.table.schema.event.SchemaChangeEvent;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.api.transform.SeaTunnelTransform;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+class AbstractMultiCatalogTransformTest {
+
+    private static final Option<String> FIELD_NAME =
+            Options.key("field_name")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Field name to append");
+
+    private static final Option<String> REQUIRED_FIELD =
+            Options.key("required_field")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Field that must exist before this rule 
is built");
+
+    private static final Option<Boolean> FILTER_SCHEMA_CHANGE_EVENT =
+            Options.key("filter_schema_change_event")
+                    .booleanType()
+                    .defaultValue(false)
+                    .withDescription("Return null for schema-change events");
+
+    private static final Option<Boolean> FAIL_ON_SCHEMA_CHANGE_EVENT =
+            Options.key("fail_on_schema_change_event")
+                    .booleanType()
+                    .defaultValue(false)
+                    .withDescription("Fail if a schema-change event reaches 
this rule");
+
+    @Test
+    void defaultModeRejectsDuplicateExactTablePathRules() {
+        CatalogTable table = catalogTable();
+        String tablePath = tablePath(table);
+        ReadonlyConfig config =
+                config(tableRule(tablePath, "first"), tableRule(tablePath, 
"second"));
+
+        IllegalStateException exception =
+                Assertions.assertThrows(
+                        IllegalStateException.class,
+                        () ->
+                                new TestMapMultiCatalogTransform(
+                                        Collections.singletonList(table), 
config));
+
+        Assertions.assertTrue(exception.getMessage().contains(tablePath));
+    }
+
+    @Test
+    void defaultModeRejectsDuplicateExactTablePathRulesBeforeTableMatching() {
+        CatalogTable table = catalogTable();
+        String duplicateTablePath = "database.schema.customers";
+        ReadonlyConfig config =
+                config(
+                        tableRule(duplicateTablePath, "first"),
+                        tableRule(duplicateTablePath, "second"));
+
+        IllegalStateException exception =
+                Assertions.assertThrows(
+                        IllegalStateException.class,
+                        () ->
+                                new TestMapMultiCatalogTransform(
+                                        Collections.singletonList(table), 
config));
+
+        
Assertions.assertTrue(exception.getMessage().contains(duplicateTablePath));
+    }
+
+    @Test
+    void firstMatchModeUsesOnlyFirstExactTablePathRule() {
+        CatalogTable table = catalogTable();
+        String tablePath = tablePath(table);
+        ReadonlyConfig config =
+                config(
+                        TransformCommonOptions.RuleMatchMode.FIRST_MATCH,
+                        tableRule(tablePath, "first"),
+                        tableRule(tablePath, "second"));
+        TestMapMultiCatalogTransform transform =
+                new 
TestMapMultiCatalogTransform(Collections.singletonList(table), config);
+
+        SeaTunnelRow output = transform.map(row(tablePath));
+
+        Assertions.assertEquals(Arrays.asList("id", "first"), 
fieldNames(transform));
+        Assertions.assertEquals(2, output.getArity());
+        Assertions.assertEquals("first", output.getField(1));
+    }
+
+    @Test
+    void allMatchModeAppliesExactTablePathRulesInDeclarationOrder() {
+        CatalogTable table = catalogTable();
+        String tablePath = tablePath(table);
+        Map<String, Object> secondRule = tableRule(tablePath, "second");
+        secondRule.put(REQUIRED_FIELD.key(), "first");
+        ReadonlyConfig config =
+                config(
+                        TransformCommonOptions.RuleMatchMode.ALL_MATCH,
+                        tableRule(tablePath, "first"),
+                        secondRule);
+        TestMapMultiCatalogTransform transform =
+                new 
TestMapMultiCatalogTransform(Collections.singletonList(table), config);
+
+        SeaTunnelRow output = transform.map(row(tablePath));
+
+        Assertions.assertEquals(Arrays.asList("id", "first", "second"), 
fieldNames(transform));
+        Assertions.assertEquals(3, output.getArity());
+        Assertions.assertEquals("first", output.getField(1));
+        Assertions.assertEquals("second", output.getField(2));
+    }
+
+    @Test
+    void allMatchModeWorksForFlatMapTransforms() {
+        CatalogTable table = catalogTable();
+        String tablePath = tablePath(table);
+        ReadonlyConfig config =
+                config(
+                        TransformCommonOptions.RuleMatchMode.ALL_MATCH,
+                        tableRule(tablePath, "first"),
+                        tableRule(tablePath, "second"));
+        TestFlatMapMultiCatalogTransform transform =
+                new 
TestFlatMapMultiCatalogTransform(Collections.singletonList(table), config);
+
+        List<SeaTunnelRow> outputRows = transform.flatMap(row(tablePath));
+
+        Assertions.assertEquals(Arrays.asList("id", "first", "second"), 
fieldNames(transform));
+        Assertions.assertEquals(1, outputRows.size());
+        Assertions.assertEquals("first", outputRows.get(0).getField(1));
+        Assertions.assertEquals("second", outputRows.get(0).getField(2));
+    }
+
+    @Test
+    void chainedMapTransformStopsSchemaChangeAfterFilteredEvent() {
+        CatalogTable table = catalogTable();
+        String tablePath = tablePath(table);
+        Map<String, Object> firstRule = tableRule(tablePath, "first");
+        firstRule.put(FILTER_SCHEMA_CHANGE_EVENT.key(), true);
+        Map<String, Object> secondRule = tableRule(tablePath, "second");
+        secondRule.put(FAIL_ON_SCHEMA_CHANGE_EVENT.key(), true);
+        ReadonlyConfig config =
+                config(TransformCommonOptions.RuleMatchMode.ALL_MATCH, 
firstRule, secondRule);
+        TestMapMultiCatalogTransform transform =
+                new 
TestMapMultiCatalogTransform(Collections.singletonList(table), config);
+
+        SchemaChangeEvent event = 
transform.mapSchemaChangeEvent(addColumnEvent(table));
+
+        Assertions.assertNull(event);
+    }
+
+    @Test
+    void chainedFlatMapTransformStopsSchemaChangeAfterFilteredEvent() {
+        CatalogTable table = catalogTable();
+        String tablePath = tablePath(table);
+        Map<String, Object> firstRule = tableRule(tablePath, "first");
+        firstRule.put(FILTER_SCHEMA_CHANGE_EVENT.key(), true);
+        Map<String, Object> secondRule = tableRule(tablePath, "second");
+        secondRule.put(FAIL_ON_SCHEMA_CHANGE_EVENT.key(), true);
+        ReadonlyConfig config =
+                config(TransformCommonOptions.RuleMatchMode.ALL_MATCH, 
firstRule, secondRule);
+        TestFlatMapMultiCatalogTransform transform =
+                new 
TestFlatMapMultiCatalogTransform(Collections.singletonList(table), config);
+
+        SchemaChangeEvent event = 
transform.mapSchemaChangeEvent(addColumnEvent(table));
+
+        Assertions.assertNull(event);
+    }
+
+    @Test
+    void tableMatchRegexIsFallbackWhenExactTablePathRuleDoesNotMatch() {
+        CatalogTable table = catalogTable();
+        String tablePath = tablePath(table);
+        Map<String, Object> configMap = new HashMap<>();
+        configMap.put(TransformCommonOptions.TABLE_MATCH_REGEX.key(), 
".*orders");
+        configMap.put(FIELD_NAME.key(), "fallback");
+        configMap.put(
+                TransformCommonOptions.MULTI_TABLES.key(),
+                
Collections.singletonList(tableRule("database.schema.customers", "specific")));
+        TestMapMultiCatalogTransform transform =
+                new TestMapMultiCatalogTransform(
+                        Collections.singletonList(table), 
ReadonlyConfig.fromMap(configMap));
+
+        SeaTunnelRow output = transform.map(row(tablePath));
+
+        Assertions.assertEquals(Arrays.asList("id", "fallback"), 
fieldNames(transform));
+        Assertions.assertEquals("fallback", output.getField(1));
+    }
+
+    @Test
+    void identityTransformIsUsedWhenNoRuleMatches() {
+        CatalogTable table = catalogTable();
+        String tablePath = tablePath(table);
+        Map<String, Object> configMap = new HashMap<>();
+        configMap.put(TransformCommonOptions.TABLE_MATCH_REGEX.key(), 
"does-not-match");
+        TestMapMultiCatalogTransform transform =
+                new TestMapMultiCatalogTransform(
+                        Collections.singletonList(table), 
ReadonlyConfig.fromMap(configMap));
+        SeaTunnelRow input = row(tablePath);
+
+        SeaTunnelRow output = transform.map(input);
+
+        Assertions.assertSame(input, output);
+        Assertions.assertEquals(Collections.singletonList("id"), 
fieldNames(transform));
+    }
+
+    private static ReadonlyConfig config(Map<String, Object>... tableRules) {
+        return config(null, tableRules);
+    }
+
+    private static ReadonlyConfig config(
+            TransformCommonOptions.RuleMatchMode ruleMatchMode, Map<String, 
Object>... tableRules) {
+        Map<String, Object> configMap = new HashMap<>();
+        if (ruleMatchMode != null) {
+            configMap.put(TransformCommonOptions.RULE_MATCH_MODE.key(), 
ruleMatchMode);
+        }
+        configMap.put(TransformCommonOptions.MULTI_TABLES.key(), 
Arrays.asList(tableRules));
+        return ReadonlyConfig.fromMap(configMap);
+    }
+
+    private static Map<String, Object> tableRule(String tablePath, String 
fieldName) {
+        Map<String, Object> rule = new HashMap<>();
+        rule.put(TransformCommonOptions.TABLE_PATH.key(), tablePath);
+        rule.put(FIELD_NAME.key(), fieldName);
+        return rule;
+    }
+
+    private static CatalogTable catalogTable() {
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"id"}, new SeaTunnelDataType[] 
{BasicType.INT_TYPE});
+        return CatalogTableUtil.getCatalogTable("catalog", "database", 
"schema", "orders", rowType);
+    }
+
+    private static String tablePath(CatalogTable table) {
+        return table.getTableId().toTablePath().toString();
+    }
+
+    private static SeaTunnelRow row(String tablePath) {
+        SeaTunnelRow row = new SeaTunnelRow(new Object[] {1});
+        row.setTableId(tablePath);
+        return row;
+    }
+
+    private static SchemaChangeEvent addColumnEvent(CatalogTable table) {
+        return AlterTableAddColumnEvent.add(
+                table.getTableId(),
+                PhysicalColumn.of("created_at", BasicType.STRING_TYPE, 0L, 
true, null, null));
+    }
+
+    private static List<String> fieldNames(SeaTunnelTransform<SeaTunnelRow> 
transform) {
+        return 
transform.getProducedCatalogTable().getTableSchema().getColumns().stream()
+                .map(Column::getName)
+                .collect(Collectors.toList());
+    }
+
+    private static SeaTunnelRow appendField(SeaTunnelRow inputRow, String 
fieldName) {
+        Object[] fields = Arrays.copyOf(inputRow.getFields(), 
inputRow.getArity() + 1);
+        fields[fields.length - 1] = fieldName;
+        SeaTunnelRow outputRow = new SeaTunnelRow(fields);
+        outputRow.setTableId(inputRow.getTableId());
+        outputRow.setRowKind(inputRow.getRowKind());
+        if (inputRow.getOptionsOrNull() != null) {
+            outputRow.setOptions(inputRow.getOptionsOrNull());
+        }
+        return outputRow;
+    }
+
+    private static TableSchema appendField(CatalogTable inputCatalogTable, 
String fieldName) {
+        List<Column> columns =
+                inputCatalogTable.getTableSchema().getColumns().stream()
+                        .map(Column::copy)
+                        .collect(Collectors.toCollection(ArrayList::new));
+        columns.add(PhysicalColumn.of(fieldName, BasicType.STRING_TYPE, 0L, 
true, null, null));
+        return TableSchema.builder()
+                .columns(columns)
+                .primaryKey(inputCatalogTable.getTableSchema().getPrimaryKey())
+                
.constraintKey(inputCatalogTable.getTableSchema().getConstraintKeys())
+                .build();
+    }
+
+    private static void assertRequiredField(CatalogTable inputCatalogTable, 
ReadonlyConfig config) {
+        config.getOptional(REQUIRED_FIELD)
+                .ifPresent(
+                        field ->
+                                inputCatalogTable
+                                        .getTableSchema()
+                                        .toPhysicalRowDataType()
+                                        .indexOf(field));
+    }
+
+    private static class TestMapMultiCatalogTransform extends 
AbstractMultiCatalogMapTransform {
+
+        private TestMapMultiCatalogTransform(
+                List<CatalogTable> inputCatalogTables, ReadonlyConfig config) {
+            super(inputCatalogTables, config);
+        }
+
+        @Override
+        public String getPluginName() {
+            return "TestMap";
+        }
+
+        @Override
+        protected SeaTunnelTransform<SeaTunnelRow> buildTransform(
+                CatalogTable inputCatalogTable, ReadonlyConfig config) {
+            return new AppendFieldMapTransform(inputCatalogTable, config);
+        }
+
+        @Override
+        protected SeaTunnelTransform<SeaTunnelRow> createIdentityTransform(
+                CatalogTable catalogTable) {
+            return new IdentityMapTransform(catalogTable);
+        }
+    }
+
+    private static class TestFlatMapMultiCatalogTransform
+            extends AbstractMultiCatalogFlatMapTransform {
+
+        private TestFlatMapMultiCatalogTransform(
+                List<CatalogTable> inputCatalogTables, ReadonlyConfig config) {
+            super(inputCatalogTables, config);
+        }
+
+        @Override
+        public String getPluginName() {
+            return "TestFlatMap";
+        }
+
+        @Override
+        protected SeaTunnelTransform<SeaTunnelRow> buildTransform(
+                CatalogTable inputCatalogTable, ReadonlyConfig config) {
+            return new AppendFieldFlatMapTransform(inputCatalogTable, config);
+        }
+
+        @Override
+        protected SeaTunnelTransform<SeaTunnelRow> createIdentityTransform(
+                CatalogTable catalogTable) {
+            return new IdentityFlatMapTransform(catalogTable);
+        }
+    }
+
+    private static class AppendFieldMapTransform extends 
AbstractCatalogSupportMapTransform {
+
+        private final String fieldName;
+
+        private final boolean filterSchemaChangeEvent;
+
+        private final boolean failOnSchemaChangeEvent;
+
+        private AppendFieldMapTransform(CatalogTable inputCatalogTable, 
ReadonlyConfig config) {
+            super(inputCatalogTable);
+            assertRequiredField(inputCatalogTable, config);
+            this.fieldName = config.get(FIELD_NAME);
+            this.filterSchemaChangeEvent = 
config.get(FILTER_SCHEMA_CHANGE_EVENT);
+            this.failOnSchemaChangeEvent = 
config.get(FAIL_ON_SCHEMA_CHANGE_EVENT);
+        }
+
+        @Override
+        public String getPluginName() {
+            return "AppendFieldMap";
+        }
+
+        @Override
+        protected SeaTunnelRow transformRow(SeaTunnelRow inputRow) {
+            return appendField(inputRow, fieldName);
+        }
+
+        @Override
+        public SchemaChangeEvent mapSchemaChangeEvent(SchemaChangeEvent event) 
{
+            if (failOnSchemaChangeEvent) {
+                throw new AssertionError("Schema-change event should not reach 
this rule");
+            }
+            if (filterSchemaChangeEvent) {
+                return null;
+            }
+            return super.mapSchemaChangeEvent(event);
+        }
+
+        @Override
+        protected TableSchema transformTableSchema() {
+            return appendField(inputCatalogTable, fieldName);
+        }
+
+        @Override
+        protected TableIdentifier transformTableIdentifier() {
+            return inputCatalogTable.getTableId().copy();
+        }
+    }
+
+    private static class AppendFieldFlatMapTransform
+            extends AbstractCatalogSupportFlatMapTransform {
+
+        private final String fieldName;
+
+        private final boolean filterSchemaChangeEvent;
+
+        private final boolean failOnSchemaChangeEvent;
+
+        private AppendFieldFlatMapTransform(CatalogTable inputCatalogTable, 
ReadonlyConfig config) {
+            super(inputCatalogTable);
+            assertRequiredField(inputCatalogTable, config);
+            this.fieldName = config.get(FIELD_NAME);
+            this.filterSchemaChangeEvent = 
config.get(FILTER_SCHEMA_CHANGE_EVENT);
+            this.failOnSchemaChangeEvent = 
config.get(FAIL_ON_SCHEMA_CHANGE_EVENT);
+        }
+
+        @Override
+        public String getPluginName() {
+            return "AppendFieldFlatMap";
+        }
+
+        @Override
+        protected List<SeaTunnelRow> transformRow(SeaTunnelRow inputRow) {
+            return Collections.singletonList(appendField(inputRow, fieldName));
+        }
+
+        @Override
+        public SchemaChangeEvent mapSchemaChangeEvent(SchemaChangeEvent event) 
{
+            if (failOnSchemaChangeEvent) {
+                throw new AssertionError("Schema-change event should not reach 
this rule");
+            }
+            if (filterSchemaChangeEvent) {
+                return null;
+            }
+            return event;
+        }
+
+        @Override
+        protected TableSchema transformTableSchema() {
+            return appendField(inputCatalogTable, fieldName);
+        }
+
+        @Override
+        protected TableIdentifier transformTableIdentifier() {
+            return inputCatalogTable.getTableId().copy();
+        }
+    }
+}
diff --git 
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/copy/CopyFieldMultiCatalogTransformTest.java
 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/copy/CopyFieldMultiCatalogTransformTest.java
new file mode 100644
index 0000000000..5429984aa7
--- /dev/null
+++ 
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/copy/CopyFieldMultiCatalogTransformTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.transform.copy;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.catalog.Column;
+import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.SeaTunnelDataType;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.api.table.type.SeaTunnelRowType;
+import org.apache.seatunnel.transform.common.TransformCommonOptions;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+class CopyFieldMultiCatalogTransformTest {
+
+    @Test
+    void allMatchModeAppliesCopyRulesInDeclarationOrder() {
+        CatalogTable table = catalogTable();
+        String tablePath = table.getTableId().toTablePath().toString();
+        CopyFieldMultiCatalogTransform transform =
+                new CopyFieldMultiCatalogTransform(
+                        Collections.singletonList(table), 
allMatchConfig(tablePath));
+        SeaTunnelRow row = new SeaTunnelRow(new Object[] {1, "name-value"});
+        row.setTableId(tablePath);
+
+        SeaTunnelRow output = transform.map(row);
+
+        Assertions.assertEquals(
+                Arrays.asList("id", "name", "name2", "name3"), 
fieldNames(transform));
+        Assertions.assertEquals(4, output.getArity());
+        Assertions.assertEquals("name-value", output.getField(2));
+        Assertions.assertEquals("name-value", output.getField(3));
+    }
+
+    private static ReadonlyConfig allMatchConfig(String tablePath) {
+        Map<String, Object> firstRule = copyRule(tablePath, "name", "name2");
+        Map<String, Object> secondRule = copyRule(tablePath, "name2", "name3");
+        Map<String, Object> config = new HashMap<>();
+        config.put(
+                TransformCommonOptions.RULE_MATCH_MODE.key(),
+                TransformCommonOptions.RuleMatchMode.ALL_MATCH);
+        config.put(TransformCommonOptions.MULTI_TABLES.key(), 
Arrays.asList(firstRule, secondRule));
+        return ReadonlyConfig.fromMap(config);
+    }
+
+    private static Map<String, Object> copyRule(
+            String tablePath, String srcField, String destField) {
+        Map<String, Object> rule = new HashMap<>();
+        rule.put(TransformCommonOptions.TABLE_PATH.key(), tablePath);
+        rule.put(CopyTransformConfig.SRC_FIELD.key(), srcField);
+        rule.put(CopyTransformConfig.DEST_FIELD.key(), destField);
+        return rule;
+    }
+
+    private static CatalogTable catalogTable() {
+        SeaTunnelRowType rowType =
+                new SeaTunnelRowType(
+                        new String[] {"id", "name"},
+                        new SeaTunnelDataType[] {BasicType.INT_TYPE, 
BasicType.STRING_TYPE});
+        return CatalogTableUtil.getCatalogTable("catalog", "database", 
"schema", "orders", rowType);
+    }
+
+    private static List<String> fieldNames(CopyFieldMultiCatalogTransform 
transform) {
+        return 
transform.getProducedCatalogTable().getTableSchema().getColumns().stream()
+                .map(Column::getName)
+                .collect(Collectors.toList());
+    }
+}

Reply via email to