This is an automated email from the ASF dual-hosted git repository.
yuzelin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 225589839 [flink] Optimization of Canal parsing logic (#1880)
225589839 is described below
commit 22558983989c34f759a6b689c4b218c912911618
Author: monster <[email protected]>
AuthorDate: Fri Aug 25 10:11:17 2023 +0800
[flink] Optimization of Canal parsing logic (#1880)
---
.../org/apache/paimon/utils/JsonSerdeUtil.java | 27 ++
.../action/cdc/kafka/formats/RecordParser.java | 4 +
.../cdc/kafka/formats/canal/CanalRecordParser.java | 288 +++++++++++++--------
.../cdc/kafka/formats/ogg/OggRecordParser.java | 8 +-
.../cdc/kafka/KafkaCanalSyncTableActionITCase.java | 2 +-
.../canal}/table/computedcolumn/canal-data-1.txt | 0
6 files changed, 213 insertions(+), 116 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/utils/JsonSerdeUtil.java
b/paimon-core/src/main/java/org/apache/paimon/utils/JsonSerdeUtil.java
index 70b87f817..0ffd1fb32 100644
--- a/paimon-core/src/main/java/org/apache/paimon/utils/JsonSerdeUtil.java
+++ b/paimon-core/src/main/java/org/apache/paimon/utils/JsonSerdeUtil.java
@@ -28,6 +28,7 @@ import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonGenerator
import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonParser;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.DeserializationContext;
+import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.JsonNode;
import org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.Module;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.SerializerProvider;
@@ -37,6 +38,7 @@ import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.ser.std.S
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.util.Optional;
/** A utility class that provide abilities for JSON serialization and
deserialization. */
public class JsonSerdeUtil {
@@ -52,6 +54,31 @@ public class JsonSerdeUtil {
OBJECT_MAPPER_INSTANCE.registerModule(createPaimonJacksonModule());
}
+ /**
+ * Retrieves a specific node from the given root node and casts it to the
specified type.
+ *
+ * @param <T> The type of the node to be returned.
+ * @param root The root node from which the specific node is to be
retrieved.
+ * @param fieldName The name of the field to retrieve.
+ * @param clazz The class of the node to be returned.
+ * @return The node casted to the specified type.
+ * @throws IllegalArgumentException if the node is not present or if it's
not of the expected
+ * type.
+ */
+ public static <T extends JsonNode> T getNodeAs(
+ JsonNode root, String fieldName, Class<T> clazz) {
+ return Optional.ofNullable(root)
+ .map(r -> r.get(fieldName))
+ .filter(clazz::isInstance)
+ .map(clazz::cast)
+ .orElseThrow(
+ () ->
+ new IllegalArgumentException(
+ String.format(
+ "Expected node '%s' to be of
type %s but was either not found or of a different type.",
+ fieldName, clazz.getName())));
+ }
+
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return OBJECT_MAPPER_INSTANCE.reader().readValue(json, clazz);
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/RecordParser.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/RecordParser.java
index cf050853b..23de278b3 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/RecordParser.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/RecordParser.java
@@ -102,4 +102,8 @@ public abstract class RecordParser implements
FlatMapFunction<String, RichCdcMul
protected String toFieldName(String rawName) {
return StringUtils.caseSensitiveConversion(rawName, caseSensitive);
}
+
+ protected Map<String, String> adjustCase(Map<String, String> map) {
+ return caseSensitive ? map : keyCaseInsensitive(map);
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/canal/CanalRecordParser.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/canal/CanalRecordParser.java
index 3b7327838..85cb07855 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/canal/CanalRecordParser.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/canal/CanalRecordParser.java
@@ -27,27 +27,40 @@ import org.apache.paimon.flink.sink.cdc.CdcRecord;
import org.apache.paimon.flink.sink.cdc.RichCdcMultiplexRecord;
import org.apache.paimon.types.DataType;
import org.apache.paimon.types.RowKind;
+import org.apache.paimon.utils.JsonSerdeUtil;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.type.TypeReference;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.JsonNode;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.node.ArrayNode;
-import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.databind.node.NullNode;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import static org.apache.paimon.utils.Preconditions.checkNotNull;
-/** Convert canal-json format string to list of {@link
RichCdcMultiplexRecord}s. */
+/**
+ * The {@code CanalRecordParser} class is responsible for parsing records from
the Canal-JSON
+ * format. Canal is a database binlog multi-platform consumer, which is used
to synchronize data
+ * across databases. This parser extracts relevant information from the
Canal-JSON format and
+ * transforms it into a list of {@link RichCdcMultiplexRecord} objects, which
represent the changes
+ * captured in the database.
+ *
+ * <p>The class handles different types of database operations such as INSERT,
UPDATE, and DELETE,
+ * and generates corresponding {@link RichCdcMultiplexRecord} objects for each
operation.
+ *
+ * <p>Additionally, the parser supports schema extraction, which can be used
to understand the
+ * structure of the incoming data and its corresponding field types.
+ */
public class CanalRecordParser extends RecordParser {
+ private static final String FIELD_IS_DDL = "isDdl";
private static final String FIELD_SQL = "sql";
private static final String FIELD_MYSQL_TYPE = "mysqlType";
private static final String FIELD_PRIMARY_KEYS = "pkNames";
@@ -70,83 +83,43 @@ public class CanalRecordParser extends RecordParser {
@Override
public List<RichCdcMultiplexRecord> extractRecords() {
+ List<RichCdcMultiplexRecord> records = new ArrayList<>();
+
if (isDdl()) {
- return Collections.emptyList();
+ return records;
}
List<String> primaryKeys = extractPrimaryKeys();
-
- // extract field types
LinkedHashMap<String, String> mySqlFieldTypes =
extractFieldTypesFromMySqlType();
- LinkedHashMap<String, DataType> paimonFieldTypes = new
LinkedHashMap<>();
- mySqlFieldTypes.forEach(
- (name, type) -> paimonFieldTypes.put(name,
MySqlTypeUtils.toDataType(type)));
+ LinkedHashMap<String, DataType> paimonFieldTypes =
+ convertToPaimonFieldTypes(mySqlFieldTypes);
- // extract row kind and field values
- List<RichCdcMultiplexRecord> records = new ArrayList<>();
String type = extractString(FIELD_TYPE);
- ArrayNode data = (ArrayNode) root.get(FIELD_DATA);
+ ArrayNode data = JsonSerdeUtil.getNodeAs(root, FIELD_DATA,
ArrayNode.class);
+
switch (type) {
case OP_UPDATE:
- ArrayNode old =
- root.get(FIELD_OLD) instanceof NullNode
- ? null
- : (ArrayNode) root.get(FIELD_OLD);
- for (int i = 0; i < data.size(); i++) {
- Map<String, String> after =
- extractRow(data.get(i), mySqlFieldTypes,
paimonFieldTypes);
- if (old != null) {
- Map<String, String> before =
- extractRow(old.get(i), mySqlFieldTypes,
paimonFieldTypes);
- // fields in "old" (before) means the fields are
changed
- // fields not in "old" (before) means the fields are
not changed,
- // so we just copy the not changed fields into before
- for (Map.Entry<String, String> entry :
after.entrySet()) {
- if (!before.containsKey(entry.getKey())) {
- before.put(entry.getKey(), entry.getValue());
- }
- }
- before = caseSensitive ? before :
keyCaseInsensitive(before);
- records.add(
- new RichCdcMultiplexRecord(
- databaseName,
- tableName,
- paimonFieldTypes,
- primaryKeys,
- new CdcRecord(RowKind.DELETE,
before)));
- }
- after = caseSensitive ? after : keyCaseInsensitive(after);
- records.add(
- new RichCdcMultiplexRecord(
- databaseName,
- tableName,
- paimonFieldTypes,
- primaryKeys,
- new CdcRecord(RowKind.INSERT, after)));
- }
- break;
+ return handleUpdateOperation(
+ data, mySqlFieldTypes, paimonFieldTypes, primaryKeys,
records);
case OP_INSERT:
- // fall through
+ return handleDataOperation(
+ data,
+ mySqlFieldTypes,
+ paimonFieldTypes,
+ primaryKeys,
+ records,
+ RowKind.INSERT);
case OP_DELETE:
- for (JsonNode datum : data) {
- Map<String, String> after =
- extractRow(datum, mySqlFieldTypes,
paimonFieldTypes);
- after = caseSensitive ? after : keyCaseInsensitive(after);
- RowKind kind = type.equals(OP_INSERT) ? RowKind.INSERT :
RowKind.DELETE;
- records.add(
- new RichCdcMultiplexRecord(
- databaseName,
- tableName,
- paimonFieldTypes,
- primaryKeys,
- new CdcRecord(kind, after)));
- }
- break;
+ return handleDataOperation(
+ data,
+ mySqlFieldTypes,
+ paimonFieldTypes,
+ primaryKeys,
+ records,
+ RowKind.DELETE);
default:
throw new UnsupportedOperationException("Unknown record type:
" + type);
}
-
- return records;
}
@Override
@@ -157,11 +130,9 @@ public class CanalRecordParser extends RecordParser {
throw new RuntimeException(e);
}
validateFormat();
-
if (isDdl()) {
return null;
}
-
LinkedHashMap<String, String> mySqlFieldTypes =
extractFieldTypesFromMySqlType();
LinkedHashMap<String, DataType> paimonFieldTypes = new
LinkedHashMap<>();
mySqlFieldTypes.forEach(
@@ -184,6 +155,7 @@ public class CanalRecordParser extends RecordParser {
checkNotNull(root.get(FIELD_TABLE), errorMessageTemplate, FIELD_TABLE);
checkNotNull(root.get(FIELD_TYPE), errorMessageTemplate, FIELD_TYPE);
checkNotNull(root.get(FIELD_DATA), errorMessageTemplate, FIELD_DATA);
+ checkNotNull(root.get(FIELD_IS_DDL), errorMessageTemplate,
FIELD_IS_DDL);
if (isDdl()) {
checkNotNull(root.get(FIELD_SQL), errorMessageTemplate, FIELD_SQL);
@@ -199,31 +171,41 @@ public class CanalRecordParser extends RecordParser {
}
private boolean isDdl() {
- return root.get("isDdl") != null && root.get("isDdl").asBoolean();
+ JsonNode isDdlNode = root.get(FIELD_IS_DDL);
+ return isDdlNode.asBoolean();
}
private List<String> extractPrimaryKeys() {
- List<String> primaryKeys = new ArrayList<>();
- ArrayNode pkNames = (ArrayNode) root.get(FIELD_PRIMARY_KEYS);
- pkNames.iterator().forEachRemaining(pk ->
primaryKeys.add(toFieldName(pk.asText())));
- return primaryKeys;
+ ArrayNode pkNames = JsonSerdeUtil.getNodeAs(root, FIELD_PRIMARY_KEYS,
ArrayNode.class);
+ return StreamSupport.stream(pkNames.spliterator(), false)
+ .map(pk -> toFieldName(pk.asText()))
+ .collect(Collectors.toList());
}
private LinkedHashMap<String, String> extractFieldTypesFromMySqlType() {
+ JsonNode schema = root.get(FIELD_MYSQL_TYPE);
LinkedHashMap<String, String> fieldTypes = new LinkedHashMap<>();
- JsonNode schema = root.get(FIELD_MYSQL_TYPE);
- Iterator<String> iterator = schema.fieldNames();
- while (iterator.hasNext()) {
- String fieldName = iterator.next();
- String fieldType = schema.get(fieldName).asText();
- fieldTypes.put(toFieldName(fieldName), fieldType);
- }
+ schema.fieldNames()
+ .forEachRemaining(
+ fieldName -> {
+ String fieldType = schema.get(fieldName).asText();
+ fieldTypes.put(toFieldName(fieldName), fieldType);
+ });
return fieldTypes;
}
- private Map<String, String> extractRow(
+ /**
+ * Extracts data from a given JSON node and transforms it based on
provided MySQL and Paimon
+ * field types.
+ *
+ * @param record The JSON node containing the data.
+ * @param mySqlFieldTypes A map of MySQL field types.
+ * @param paimonFieldTypes A map of Paimon field types.
+ * @return A map of extracted and transformed data.
+ */
+ private Map<String, String> extractRowFromJson(
JsonNode record,
Map<String, String> mySqlFieldTypes,
LinkedHashMap<String, DataType> paimonFieldTypes) {
@@ -233,35 +215,19 @@ public class CanalRecordParser extends RecordParser {
return new HashMap<>();
}
- Map<String, String> resultMap = new HashMap<>();
- for (Map.Entry<String, String> field : mySqlFieldTypes.entrySet()) {
- String fieldName = field.getKey();
- String mySqlType = field.getValue();
- Object objectValue = jsonMap.get(fieldName);
- if (objectValue == null) {
- continue;
- }
-
- String oldValue = objectValue.toString();
- String newValue = oldValue;
-
- if
(MySqlTypeUtils.isSetType(MySqlTypeUtils.getShortType(mySqlType))) {
- newValue = CanalFieldParser.convertSet(newValue, mySqlType);
- } else if
(MySqlTypeUtils.isEnumType(MySqlTypeUtils.getShortType(mySqlType))) {
- newValue = CanalFieldParser.convertEnum(newValue, mySqlType);
- } else if
(MySqlTypeUtils.isGeoType(MySqlTypeUtils.getShortType(mySqlType))) {
- try {
- byte[] wkb =
- CanalFieldParser.convertGeoType2WkbArray(
-
oldValue.getBytes(StandardCharsets.ISO_8859_1));
- newValue = MySqlTypeUtils.convertWkbArray(wkb);
- } catch (Exception e) {
- throw new IllegalArgumentException(
- String.format("Failed to convert %s to geometry
JSON.", oldValue), e);
- }
- }
- resultMap.put(fieldName, newValue);
- }
+ Map<String, String> resultMap =
+ mySqlFieldTypes.entrySet().stream()
+ .filter(
+ entry ->
+ jsonMap.containsKey(entry.getKey())
+ && jsonMap.get(entry.getKey())
!= null)
+ .collect(
+ Collectors.toMap(
+ Map.Entry::getKey,
+ entry ->
+ transformValue(
+
jsonMap.get(entry.getKey()).toString(),
+ entry.getValue())));
// generate values for computed columns
for (ComputedColumn computedColumn : computedColumns) {
@@ -273,4 +239,104 @@ public class CanalRecordParser extends RecordParser {
return resultMap;
}
+
+ private String transformValue(String oldValue, String mySqlType) {
+ String shortType = MySqlTypeUtils.getShortType(mySqlType);
+
+ if (MySqlTypeUtils.isSetType(shortType)) {
+ return CanalFieldParser.convertSet(oldValue, mySqlType);
+ }
+
+ if (MySqlTypeUtils.isEnumType(shortType)) {
+ return CanalFieldParser.convertEnum(oldValue, mySqlType);
+ }
+
+ if (MySqlTypeUtils.isGeoType(shortType)) {
+ try {
+ byte[] wkb =
+ CanalFieldParser.convertGeoType2WkbArray(
+
oldValue.getBytes(StandardCharsets.ISO_8859_1));
+ return MySqlTypeUtils.convertWkbArray(wkb);
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ String.format("Failed to convert %s to geometry
JSON.", oldValue), e);
+ }
+ }
+ return oldValue;
+ }
+
+ private List<RichCdcMultiplexRecord> handleDataOperation(
+ ArrayNode data,
+ Map<String, String> mySqlFieldTypes,
+ LinkedHashMap<String, DataType> paimonFieldTypes,
+ List<String> primaryKeys,
+ List<RichCdcMultiplexRecord> records,
+ RowKind kind) {
+ for (JsonNode datum : data) {
+ Map<String, String> rowData =
+ extractRowFromJson(datum, mySqlFieldTypes,
paimonFieldTypes);
+ rowData = adjustCase(rowData);
+
+ records.add(
+ new RichCdcMultiplexRecord(
+ databaseName,
+ tableName,
+ paimonFieldTypes,
+ primaryKeys,
+ new CdcRecord(kind, rowData)));
+ }
+ return records;
+ }
+
+ private List<RichCdcMultiplexRecord> handleUpdateOperation(
+ ArrayNode data,
+ Map<String, String> mySqlFieldTypes,
+ LinkedHashMap<String, DataType> paimonFieldTypes,
+ List<String> primaryKeys,
+ List<RichCdcMultiplexRecord> records) {
+ ArrayNode old = JsonSerdeUtil.getNodeAs(root, FIELD_OLD,
ArrayNode.class);
+ for (int i = 0; i < data.size(); i++) {
+ Map<String, String> after =
+ extractRowFromJson(data.get(i), mySqlFieldTypes,
paimonFieldTypes);
+
+ if (old != null && i < old.size()) {
+ Map<String, String> before =
+ extractRowFromJson(old.get(i), mySqlFieldTypes,
paimonFieldTypes);
+
+ // Fields in "old" (before) means the fields are changed.
+ // Fields not in "old" (before) means the fields are not
changed,
+ // so we just copy the not changed fields into before.
+ for (Map.Entry<String, String> entry : after.entrySet()) {
+ before.putIfAbsent(entry.getKey(), entry.getValue());
+ }
+
+ before = adjustCase(before);
+ records.add(
+ new RichCdcMultiplexRecord(
+ databaseName,
+ tableName,
+ paimonFieldTypes,
+ primaryKeys,
+ new CdcRecord(RowKind.DELETE, before)));
+ }
+
+ after = adjustCase(after);
+ records.add(
+ new RichCdcMultiplexRecord(
+ databaseName,
+ tableName,
+ paimonFieldTypes,
+ primaryKeys,
+ new CdcRecord(RowKind.INSERT, after)));
+ }
+ return records;
+ }
+
+ private LinkedHashMap<String, DataType> convertToPaimonFieldTypes(
+ Map<String, String> mySqlFieldTypes) {
+ LinkedHashMap<String, DataType> paimonFieldTypes = new
LinkedHashMap<>();
+ mySqlFieldTypes.forEach(
+ (name, type) -> paimonFieldTypes.put(name,
MySqlTypeUtils.toDataType(type)));
+ return paimonFieldTypes;
+ }
}
diff --git
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/ogg/OggRecordParser.java
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/ogg/OggRecordParser.java
index 9d529849c..19d0a1e70 100644
---
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/ogg/OggRecordParser.java
+++
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/action/cdc/kafka/formats/ogg/OggRecordParser.java
@@ -27,6 +27,7 @@ import
org.apache.paimon.flink.sink.cdc.RichCdcMultiplexRecord;
import org.apache.paimon.types.DataType;
import org.apache.paimon.types.DataTypes;
import org.apache.paimon.types.RowKind;
+import org.apache.paimon.utils.JsonSerdeUtil;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.JsonProcessingException;
import
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.core.type.TypeReference;
@@ -93,7 +94,7 @@ public class OggRecordParser extends RecordParser {
JsonNode jsonNode, RowKind rowKind, List<RichCdcMultiplexRecord>
records) {
LinkedHashMap<String, DataType> paimonFieldTypes = new
LinkedHashMap<>();
Map<String, String> rowData = extractRow(jsonNode, paimonFieldTypes);
- rowData = caseSensitive ? rowData : keyCaseInsensitive(rowData);
+ rowData = adjustCase(rowData);
records.add(createRecord(rowKind, rowData, paimonFieldTypes));
}
@@ -163,7 +164,7 @@ public class OggRecordParser extends RecordParser {
}
private void extractDatabaseAndTableNames() {
- JsonNode tableNode = root.get(FIELD_TABLE);
+ JsonNode tableNode = JsonSerdeUtil.getNodeAs(root, FIELD_TABLE,
JsonNode.class);
if (tableNode != null) {
String[] dbt = tableNode.asText().split("\\.", 2); // Limit split
to 2 parts
if (dbt.length == 2) {
@@ -175,8 +176,7 @@ public class OggRecordParser extends RecordParser {
private LinkedHashMap<String, DataType> extractFieldTypesFromOracleType() {
LinkedHashMap<String, DataType> fieldTypes = new LinkedHashMap<>();
-
- JsonNode record = root.get(FIELD_AFTER);
+ JsonNode record = JsonSerdeUtil.getNodeAs(root, FIELD_AFTER,
JsonNode.class);
Map<String, Object> linkedHashMap =
OBJECT_MAPPER.convertValue(
record, new TypeReference<LinkedHashMap<String,
Object>>() {});
diff --git
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/cdc/kafka/KafkaCanalSyncTableActionITCase.java
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/cdc/kafka/KafkaCanalSyncTableActionITCase.java
index a30527261..c3b2b86dd 100644
---
a/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/cdc/kafka/KafkaCanalSyncTableActionITCase.java
+++
b/paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/cdc/kafka/KafkaCanalSyncTableActionITCase.java
@@ -1069,7 +1069,7 @@ public class KafkaCanalSyncTableActionITCase extends
KafkaActionITCaseBase {
String topic = "computed_column";
createTestTopic(topic, 1, 1);
- List<String> lines =
readLines("kafka.canal/table/computedcolumn/canal-data-1.txt");
+ List<String> lines =
readLines("kafka/canal/table/computedcolumn/canal-data-1.txt");
try {
writeRecordsToKafka(topic, lines);
} catch (Exception e) {
diff --git
a/paimon-flink/paimon-flink-common/src/test/resources/kafka.canal/table/computedcolumn/canal-data-1.txt
b/paimon-flink/paimon-flink-common/src/test/resources/kafka/canal/table/computedcolumn/canal-data-1.txt
similarity index 100%
rename from
paimon-flink/paimon-flink-common/src/test/resources/kafka.canal/table/computedcolumn/canal-data-1.txt
rename to
paimon-flink/paimon-flink-common/src/test/resources/kafka/canal/table/computedcolumn/canal-data-1.txt