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 f3e1c2ee3b [Fix][Transform-V2] Rebind Replace fields on schema refresh
(#12365)
f3e1c2ee3b is described below
commit f3e1c2ee3b83efd4a1e7fb4cc4fe40b44060c7d0
Author: Goutam Adwant <[email protected]>
AuthorDate: Sat Oct 31 12:53:59 2026 +0000
[Fix][Transform-V2] Rebind Replace fields on schema refresh (#12365)
Signed-off-by: Goutam Adwant <[email protected]>
---
.../introduction/concepts/incompatible-changes.md | 5 +
docs/en/transforms/replace.md | 5 +-
.../introduction/concepts/incompatible-changes.md | 5 +
docs/zh/transforms/replace.md | 4 +
.../cdc/mysql/MysqlCDCWithSchemaChangeIT.java | 144 +++++++++++++
...sqlcdc_to_mysql_with_replace_schema_change.conf | 59 ++++++
.../transform/replace/ReplaceTransform.java | 2 +
.../transform/replace/ReplaceTransformTest.java | 230 +++++++++++++++++++++
8 files changed, 453 insertions(+), 1 deletion(-)
diff --git a/docs/en/introduction/concepts/incompatible-changes.md
b/docs/en/introduction/concepts/incompatible-changes.md
index 41e06d61f2..62a1967254 100644
--- a/docs/en/introduction/concepts/incompatible-changes.md
+++ b/docs/en/introduction/concepts/incompatible-changes.md
@@ -272,6 +272,11 @@ The Zeta SQL ARRAY function now declares TINYINT elements
as `ARRAY<TINYINT>`, m
(`existingConfigMap`) and restrict the member port (5801) with a
`NetworkPolicy`. Restart the pods
after upgrading so the new configuration is applied.
+### Replace Transform Schema Refresh
+
+- **Behavior change**: Replace resolves `replace_fields` against the refreshed
input schema after schema changes and checkpoint restoration. Previously cached
positions could replace an unrelated column or leave the configured field
unchanged after column positions shifted.
+- **Impact and migration**: If a configured field was dropped or renamed,
schema refresh now fails with the existing missing-field error instead of
processing subsequent rows with stale positions. Update `replace_fields` to
match the source schema before restarting. No option, default, API, or
checkpoint format changes are introduced.
+
### SQL and Calcite Vector Arithmetic
- **Behavior correction**: `VECTOR_NORM`, `INNER_PRODUCT`, `COSINE_DISTANCE`,
diff --git a/docs/en/transforms/replace.md b/docs/en/transforms/replace.md
index aba1c3a193..e50bf746b0 100644
--- a/docs/en/transforms/replace.md
+++ b/docs/en/transforms/replace.md
@@ -24,6 +24,10 @@ The fields you want to replace
For backward compatibility, legacy key `replace_field = "name"` is still
supported, but new configurations should use `replace_fields`.
+Field names are resolved again when the input schema is refreshed, including
checkpoint schema restoration. Adding, removing, or reordering other columns
therefore does not change which fields receive replacements. If a configured
field is absent from the refreshed schema (for example, it was dropped or
renamed), the refresh fails with a missing-field error before subsequent rows
are processed. Update `replace_fields` to match the source schema before
restarting the job.
+
+This field-binding behavior does not change the engine's schema-evolution
support or the existing value-conversion and output-schema behavior of Replace.
+
### pattern [string]
The old string that will be replaced
@@ -122,4 +126,3 @@ sink {
### new version
- Add Replace Transform Connector
-
diff --git a/docs/zh/introduction/concepts/incompatible-changes.md
b/docs/zh/introduction/concepts/incompatible-changes.md
index 7db9438db4..0c5342f45c 100644
--- a/docs/zh/introduction/concepts/incompatible-changes.md
+++ b/docs/zh/introduction/concepts/incompatible-changes.md
@@ -221,6 +221,11 @@ Zeta SQL ARRAY 函数现在将 TINYINT 元素声明为 `ARRAY<TINYINT>`,与实
并使用 `schema_save_mode = "ERROR_WHEN_SCHEMA_NOT_EXIST"` 保留该表结构。不要对手动定义的目标表使用
`RECREATE_SCHEMA`。任意指定索引前缀长度可能拒绝前缀相同但完整值不同的源主键,因此无法保持原有主键语义。
+### Replace 转换的模式刷新
+
+- **行为变更**:模式变更或检查点恢复后,Replace 会根据刷新的输入模式重新解析
`replace_fields`。此前,列位置变化后,缓存的位置可能导致替换无关列或漏掉配置的字段。
+- **影响和迁移**:如果配置的字段已被删除或重命名,模式刷新现在会报出现有的字段不存在错误,而不是使用过期位置继续处理数据行。重新启动作业之前,请更新
`replace_fields`,使其与源表模式一致。此更改不涉及选项、默认值、API 或检查点格式变更。
+
### SQL 和 Calcite 向量运算
- **行为修正**:`VECTOR_NORM`、`INNER_PRODUCT`、`COSINE_DISTANCE`、`L1_DISTANCE`、
diff --git a/docs/zh/transforms/replace.md b/docs/zh/transforms/replace.md
index b1222f04ed..466e4dc35e 100644
--- a/docs/zh/transforms/replace.md
+++ b/docs/zh/transforms/replace.md
@@ -24,6 +24,10 @@
为了向后兼容,旧配置 `replace_field = "name"` 仍然支持,但新配置建议统一使用 `replace_fields`。
+输入模式刷新时(包括从检查点恢复模式),会重新解析配置的字段名。因此,添加、删除或重排其他列不会改变需要执行替换的字段。如果配置的字段在刷新后的模式中不存在(例如已被删除或重命名),刷新会在处理后续数据行之前报出字段不存在错误。重新启动作业之前,请更新
`replace_fields`,使其与源表模式一致。
+
+此字段绑定行为不会改变引擎对模式演变的支持,也不会改变 Replace 现有的值转换和输出模式行为。
+
### pattern [string]
将被替换的旧字符串
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCWithSchemaChangeIT.java
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCWithSchemaChangeIT.java
index ad1fd293ee..fdf60ce136 100644
---
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCWithSchemaChangeIT.java
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/java/org/apache/seatunnel/connectors/seatunnel/cdc/mysql/MysqlCDCWithSchemaChangeIT.java
@@ -94,6 +94,9 @@ public class MysqlCDCWithSchemaChangeIT extends TestSuiteBase
implements TestRes
/** Dedicated sink table used by the event-type filter regression
coverage. */
private static final String SINK_TABLE_FILTER =
"mysql_cdc_e2e_sink_table_schema_change_filter";
+ private static final String REPLACE_SOURCE_TABLE = "replace_schema_source";
+ private static final String REPLACE_SINK_TABLE = "replace_schema_sink";
+
/** Stable projection used after add-column evolution to compare source
and sink rows. */
private static final String STABLE_QUERY =
"select id,name,description,weight from %s.%s order by id";
@@ -264,6 +267,147 @@ public class MysqlCDCWithSchemaChangeIT extends
TestSuiteBase implements TestRes
MYSQL_DATABASE, SOURCE_TABLE, "Updated product catalog with
sports equipment");
}
+ /** Verifies field binding after a live position shift and subsequent
checkpoint restoration. */
+ @Order(5)
+ @TestTemplate
+ public void
testReplaceFieldBindingAfterSchemaChangeAndRestore(TestContainer container)
+ throws Exception {
+ try (Connection connection = getJdbcConnection();
+ Statement statement = connection.createStatement()) {
+ statement.execute("DROP TABLE IF EXISTS shop." +
REPLACE_SOURCE_TABLE);
+ statement.execute("DROP TABLE IF EXISTS shop." +
REPLACE_SINK_TABLE);
+ statement.execute(
+ "CREATE TABLE shop."
+ + REPLACE_SOURCE_TABLE
+ + " (guard_value VARCHAR(64), name VARCHAR(64),
title VARCHAR(64), id INT PRIMARY KEY)");
+ statement.execute(
+ "CREATE TABLE shop."
+ + REPLACE_SINK_TABLE
+ + " LIKE shop."
+ + REPLACE_SOURCE_TABLE);
+ statement.execute(
+ "INSERT INTO shop."
+ + REPLACE_SOURCE_TABLE
+ + " VALUES ('before guard', 'before name', 'before
title', 1)");
+ }
+ String jobId = String.valueOf(JobIdGenerator.newJobId());
+ String config = "/mysqlcdc_to_mysql_with_replace_schema_change.conf";
+ CompletableFuture<?> execution =
+ CompletableFuture.runAsync(
+ () -> {
+ try {
+ Assertions.assertEquals(
+ 0, container.executeJob(config,
jobId).getExitCode());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ Throwable testFailure = null;
+ try {
+ assertReplaceRow(execution, "'before prefix'", "after name",
"after title");
+ waitForReplaceIncrementalRead(container, execution);
+ try (Connection connection = getJdbcConnection();
+ Statement statement = connection.createStatement()) {
+ statement.execute(
+ "ALTER TABLE shop."
+ + REPLACE_SOURCE_TABLE
+ + " ADD COLUMN prefix VARCHAR(64) DEFAULT
'before prefix' FIRST");
+ statement.execute(
+ "UPDATE shop."
+ + REPLACE_SOURCE_TABLE
+ + " SET name='before live', title='before
changed' WHERE id=1");
+ }
+ assertReplaceRow(execution, "prefix", "after live", "after
changed");
+ Assertions.assertEquals(0,
container.savepointJob(jobId).getExitCode());
+ execution.get(60, TimeUnit.SECONDS);
+ try (Connection connection = getJdbcConnection();
+ Statement statement = connection.createStatement()) {
+ statement.execute(
+ "UPDATE shop."
+ + REPLACE_SOURCE_TABLE
+ + " SET name='before restored', title='before
recovered' WHERE id=1");
+ }
+ execution =
+ CompletableFuture.runAsync(
+ () -> {
+ try {
+ Assertions.assertEquals(
+ 0, container.restoreJob(config,
jobId).getExitCode());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+ assertReplaceRow(execution, "prefix", "after restored", "after
recovered");
+ } catch (Exception | AssertionError failure) {
+ testFailure = failure;
+ throw failure;
+ } finally {
+ try {
+ try {
+ container.cancelJob(jobId);
+ } finally {
+ execution.get(60, TimeUnit.SECONDS);
+ }
+ } catch (Exception | AssertionError cleanupFailure) {
+ if (testFailure != null) {
+ testFailure.addSuppressed(cleanupFailure);
+ } else {
+ throw cleanupFailure;
+ }
+ }
+ }
+ }
+
+ private void waitForReplaceIncrementalRead(
+ TestContainer container, CompletableFuture<?> execution) {
+ await().atMost(DEFAULT_TABLE_SYNC_TIMEOUT_MS, TimeUnit.MILLISECONDS)
+ .untilAsserted(
+ () -> {
+ if (execution.isCompletedExceptionally()) {
+ execution.join();
+ }
+ Assertions.assertTrue(
+
Arrays.stream(container.getServerLogs().split("\\R"))
+ .anyMatch(
+ line ->
+
line.contains(INCREMENTAL_READ_MARKER)
+ &&
line.contains(
+
MYSQL_DATABASE
+
+ "."
+
+ REPLACE_SOURCE_TABLE)),
+ "The Replace source has not started
incremental reading");
+ });
+ }
+
+ private void assertReplaceRow(
+ CompletableFuture<?> execution, String prefixExpression, String
name, String title) {
+ await().atMost(STRUCTURE_AND_DATA_ASSERT_TIMEOUT_MILLIS,
TimeUnit.MILLISECONDS)
+ .ignoreExceptionsMatching(
+ error ->
+ error.getCause() instanceof SQLException
+ && ((SQLException)
error.getCause()).getErrorCode() == 1054)
+ .untilAsserted(
+ () -> {
+ if (execution.isCompletedExceptionally()) {
+ execution.join();
+ }
+ Assertions.assertEquals(
+ Arrays.asList(
+ Arrays.asList(
+ "before prefix",
+ "before guard",
+ name,
+ title,
+ 1)),
+ query(
+ "SELECT "
+ + prefixExpression
+ +
",guard_value,name,title,id FROM shop."
+ + REPLACE_SINK_TABLE
+ + " ORDER BY id"));
+ });
+ }
+
@Order(2)
@TestTemplate
public void testMysqlCdcWithSchemaEvolutionCaseExactlyOnce(TestContainer
container) {
diff --git
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_to_mysql_with_replace_schema_change.conf
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_to_mysql_with_replace_schema_change.conf
new file mode 100644
index 0000000000..b076c43d38
--- /dev/null
+++
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-cdc-mysql-e2e/src/test/resources/mysqlcdc_to_mysql_with_replace_schema_change.conf
@@ -0,0 +1,59 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+env {
+ parallelism = 1
+ job.mode = "STREAMING"
+ checkpoint.interval = 5000
+}
+
+source {
+ MySQL-CDC {
+ plugin_output = "source"
+ server-id = 5852-5853
+ username = "st_user_source"
+ password = "mysqlpw"
+ table-names = ["shop.replace_schema_source"]
+ url = "jdbc:mysql://mysql_cdc_e2e:3306/shop"
+ schema-changes.enabled = true
+ }
+}
+
+transform {
+ Replace {
+ plugin_input = "source"
+ plugin_output = "replaced"
+ replace_fields = ["name", "title"]
+ pattern = "before"
+ replacement = "after"
+ }
+}
+
+sink {
+ jdbc {
+ plugin_input = "replaced"
+ url = "jdbc:mysql://mysql_cdc_e2e:3306/shop"
+ driver = "com.mysql.cj.jdbc.Driver"
+ user = "st_user_sink"
+ password = "mysqlpw"
+ generate_sink_sql = true
+ database = shop
+ table = replace_schema_sink
+ primary_keys = ["id"]
+ multi_table_sink_replica = 1
+ }
+}
diff --git
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransform.java
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransform.java
index 0ceb749667..0864d1006a 100644
---
a/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransform.java
+++
b/seatunnel-transforms-v2/src/main/java/org/apache/seatunnel/transform/replace/ReplaceTransform.java
@@ -119,6 +119,8 @@ public class ReplaceTransform extends
AbstractCatalogSupportMapTransform {
@Override
protected TableSchema transformTableSchema() {
+ // Schema refresh and input handoff must rebuild the physical field
bindings.
+ initializeFieldIndexes();
return inputCatalogTable.getTableSchema();
}
diff --git
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformTest.java
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformTest.java
index a823c77c9f..a6af50bdb4 100644
---
a/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformTest.java
+++
b/seatunnel-transforms-v2/src/test/java/org/apache/seatunnel/transform/replace/ReplaceTransformTest.java
@@ -26,7 +26,14 @@ import org.apache.seatunnel.api.table.catalog.PhysicalColumn;
import org.apache.seatunnel.api.table.catalog.TableIdentifier;
import org.apache.seatunnel.api.table.catalog.TablePath;
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.AlterTableChangeColumnEvent;
+import org.apache.seatunnel.api.table.schema.event.AlterTableDropColumnEvent;
+import org.apache.seatunnel.api.table.schema.event.AlterTableModifyColumnEvent;
+import org.apache.seatunnel.api.table.schema.event.RestoreTableSchemaEvent;
+import org.apache.seatunnel.api.table.schema.event.SchemaChangeEvent;
import org.apache.seatunnel.api.table.type.BasicType;
+import org.apache.seatunnel.api.table.type.RowKind;
import org.apache.seatunnel.api.table.type.SeaTunnelRow;
import org.apache.seatunnel.common.exception.SeaTunnelRuntimeException;
import org.apache.seatunnel.transform.exception.TransformException;
@@ -37,6 +44,7 @@ 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.Map;
@@ -44,6 +52,228 @@ class ReplaceTransformTest {
private static CatalogTable catalogTable;
+ @Test
+ void testRestoreSchemaRebindsFieldsBeforeNextRow() {
+ ReplaceTransform transform = replacementTransform("name", "title");
+ transform.getProducedCatalogTable();
+ CatalogTable restored = reorderedTable("title", "id", "name");
+ RestoreTableSchemaEvent event = new RestoreTableSchemaEvent(restored);
+
+ Assertions.assertSame(event, transform.mapSchemaChangeEvent(event));
+ Assertions.assertArrayEquals(
+ new Object[] {"after title", 1, "after name"},
+ transform
+ .map(new SeaTunnelRow(new Object[] {"before title", 1,
"before name"}))
+ .getFields());
+ Assertions.assertEquals(restored.getTableSchema(),
event.getChangeAfter().getTableSchema());
+ }
+
+ @Test
+ void testInputSchemaHandoffRebindsFieldsBeforeNextRow() {
+ ReplaceTransform transform = replacementTransform("name", "title");
+ transform.getProducedCatalogTable();
+ CatalogTable reordered = reorderedTable("title", "name", "id");
+ transform.setInputCatalogTable(reordered);
+
+ Assertions.assertArrayEquals(
+ new Object[] {"after title", "after name", 1},
+ transform
+ .map(new SeaTunnelRow(new Object[] {"before title",
"before name", 1}))
+ .getFields());
+ Assertions.assertEquals(
+ reordered.getTableSchema(),
transform.getProducedCatalogTable().getTableSchema());
+ }
+
+ private static ReplaceTransform replacementTransform(String... fields) {
+ return new ReplaceTransform(replacementConfig(fields), catalogTable);
+ }
+
+ private static ReadonlyConfig replacementConfig(String... fields) {
+ Map<String, Object> config = new HashMap<>();
+ config.put("replace_fields", Arrays.asList(fields));
+ config.put("pattern", "before");
+ config.put("replacement", "after");
+ return ReadonlyConfig.fromMap(config);
+ }
+
+ private static CatalogTable reorderedTable(String... fields) {
+ TableSchema.Builder schema = TableSchema.builder();
+ for (String field : fields) {
+ schema.column(catalogTable.getTableSchema().getColumn(field));
+ }
+ return CatalogTable.of(
+ catalogTable.getTableId(),
+ schema.build(),
+ catalogTable.getOptions(),
+ catalogTable.getPartitionKeys(),
+ catalogTable.getComment());
+ }
+
+ @Test
+ void testAddDropAndReorderRebindFields() {
+ ReplaceTransform transform = replacementTransform("name", "title");
+ transform.mapSchemaChangeEvent(
+ AlterTableAddColumnEvent.addFirst(
+ catalogTable.getTableId(),
+ PhysicalColumn.of("prefix", BasicType.STRING_TYPE,
10L, true, null, null)));
+ Assertions.assertArrayEquals(
+ new Object[] {"before prefix", 1, "after name", "after title"},
+ transform
+ .map(
+ new SeaTunnelRow(
+ new Object[] {
+ "before prefix", 1, "before name",
"before title"
+ }))
+ .getFields());
+
+ transform.mapSchemaChangeEvent(
+ new AlterTableDropColumnEvent(catalogTable.getTableId(),
"id"));
+ Assertions.assertArrayEquals(
+ new Object[] {"before prefix", "after name", "after title"},
+ transform
+ .map(
+ new SeaTunnelRow(
+ new Object[] {
+ "before prefix", "before name",
"before title"
+ }))
+ .getFields());
+
+ transform.mapSchemaChangeEvent(
+ AlterTableModifyColumnEvent.modifyFirst(
+ catalogTable.getTableId(),
+ catalogTable.getTableSchema().getColumn("title")));
+ Assertions.assertArrayEquals(
+ new Object[] {"after title", "before prefix", "after name"},
+ transform
+ .map(
+ new SeaTunnelRow(
+ new Object[] {
+ "before title", "before prefix",
"before name"
+ }))
+ .getFields());
+ }
+
+ @Test
+ void testMissingTargetFailsDuringSchemaRefresh() {
+ SchemaChangeEvent[] events = {
+ new RestoreTableSchemaEvent(reorderedTable("id", "title")),
+ new AlterTableDropColumnEvent(catalogTable.getTableId(), "name"),
+ AlterTableChangeColumnEvent.change(
+ catalogTable.getTableId(),
+ "name",
+ PhysicalColumn.of("renamed", BasicType.STRING_TYPE, 10L,
true, null, null))
+ };
+ for (SchemaChangeEvent event : events) {
+ ReplaceTransform transform = replacementTransform("title", "name");
+ TransformException error =
+ Assertions.assertThrows(
+ TransformException.class, () ->
transform.mapSchemaChangeEvent(event));
+ Assertions.assertTrue(error.getMessage().contains("name"));
+ }
+ ReplaceTransform transform = replacementTransform("name");
+ Assertions.assertThrows(
+ TransformException.class,
+ () -> transform.setInputCatalogTable(reorderedTable("id",
"title")));
+ }
+
+ @Test
+ void testRestorePreservesRowMetadataNullsAndInput() {
+ ReplaceTransform transform = replacementTransform("name", "title");
+ transform.mapSchemaChangeEvent(
+ new RestoreTableSchemaEvent(reorderedTable("title", "id",
"name")));
+ SeaTunnelRow input = new SeaTunnelRow(new Object[] {null, 1, "before
name"});
+ input.setRowKind(RowKind.UPDATE_AFTER);
+ input.setTableId(catalogTable.getTableId().toTablePath().toString());
+ input.getOptions().put("sequence", 123L);
+ SeaTunnelRow output = transform.map(input);
+ Assertions.assertArrayEquals(new Object[] {null, 1, "after name"},
output.getFields());
+ Assertions.assertArrayEquals(new Object[] {null, 1, "before name"},
input.getFields());
+ Assertions.assertEquals(input.getRowKind(), output.getRowKind());
+ Assertions.assertEquals(input.getTableId(), output.getTableId());
+ Assertions.assertEquals(input.getOptions(), output.getOptions());
+ }
+
+ @Test
+ void testTypeChangeKeepsExistingValueConversion() {
+ ReplaceTransform transform = replacementTransform("name");
+ transform.mapSchemaChangeEvent(
+ AlterTableModifyColumnEvent.modifyFirst(
+ catalogTable.getTableId(),
+ PhysicalColumn.of("name", BasicType.INT_TYPE, 10L,
true, null, null)));
+ Assertions.assertArrayEquals(
+ new Object[] {"123", 1, "before title"},
+ transform.map(new SeaTunnelRow(new Object[] {123, 1, "before
title"})).getFields());
+ Assertions.assertEquals(
+ BasicType.INT_TYPE,
+
transform.getProducedCatalogTable().getSeaTunnelRowType().getFieldType(0));
+ }
+
+ @Test
+ void testMultiTableRestoreOnlyRebindsAffectedTable() {
+ CatalogTable second =
+ CatalogTable.of(
+ TableIdentifier.of("catalog", TablePath.of("other")),
+ catalogTable.getTableSchema(),
+ new HashMap<>(),
+ new ArrayList<>(),
+ "other");
+ ReplaceMultiCatalogTransform transform =
+ new ReplaceMultiCatalogTransform(
+ Arrays.asList(catalogTable, second),
replacementConfig("name", "title"));
+ CatalogTable restored = reorderedTable("title", "id", "name");
+ transform.mapSchemaChangeEvent(new RestoreTableSchemaEvent(restored));
+
+ SeaTunnelRow firstInput = new SeaTunnelRow(new Object[] {"before
title", 1, "before name"});
+
firstInput.setTableId(catalogTable.getTableId().toTablePath().toString());
+ Assertions.assertArrayEquals(
+ new Object[] {"after title", 1, "after name"},
+ transform.map(firstInput).getFields());
+ SeaTunnelRow secondInput =
+ new SeaTunnelRow(new Object[] {2, "before other", "before
title"});
+ secondInput.setTableId(second.getTableId().toTablePath().toString());
+ Assertions.assertArrayEquals(
+ new Object[] {2, "after other", "after title"},
+ transform.map(secondInput).getFields());
+ Assertions.assertEquals(
+ restored.getTableSchema(),
+ transform.getProducedCatalogTables().get(0).getTableSchema());
+ Assertions.assertEquals(
+ second.getTableSchema(),
+ transform.getProducedCatalogTables().get(1).getTableSchema());
+ }
+
+ @Test
+ void testAllMatchChainRebindsOnRestoreAndInputHandoff() {
+ Map<String, Object> firstRule = new HashMap<>();
+ firstRule.put("table_path",
catalogTable.getTableId().toTablePath().toString());
+ firstRule.put("replace_fields", Arrays.asList("name", "title"));
+ firstRule.put("pattern", "before");
+ firstRule.put("replacement", "middle");
+ Map<String, Object> secondRule = new HashMap<>(firstRule);
+ secondRule.put("pattern", "middle");
+ secondRule.put("replacement", "after");
+ Map<String, Object> config = new HashMap<>();
+ config.put("rule_match_mode", "ALL_MATCH");
+ config.put("table_transform", Arrays.asList(firstRule, secondRule));
+ ReplaceMultiCatalogTransform transform =
+ new ReplaceMultiCatalogTransform(
+ Collections.singletonList(catalogTable),
ReadonlyConfig.fromMap(config));
+ transform.mapSchemaChangeEvent(
+ new RestoreTableSchemaEvent(reorderedTable("title", "id",
"name")));
+ Assertions.assertArrayEquals(
+ new Object[] {"after title", 1, "after name"},
+ transform
+ .map(new SeaTunnelRow(new Object[] {"before title", 1,
"before name"}))
+ .getFields());
+ transform.setInputCatalogTables(
+ Collections.singletonList(reorderedTable("name", "title",
"id")));
+ Assertions.assertArrayEquals(
+ new Object[] {"after name", "after title", 1},
+ transform
+ .map(new SeaTunnelRow(new Object[] {"before name",
"before title", 1}))
+ .getFields());
+ }
+
@BeforeAll
static void setUp() {
catalogTable =