wenshao commented on code in PR #4374:
URL: https://github.com/apache/flink-cdc/pull/4374#discussion_r3069186886


##########
flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/serializer/data/writer/BinaryWriter.java:
##########
@@ -171,7 +177,25 @@ static void write(
                 writer.writeMap(pos, (MapData) o, (MapDataSerializer) 
serializer);
                 break;
             case ROW:
-                writer.writeRecord(pos, (RecordData) o, 
(TypeSerializer<RecordData>) serializer);
+                RecordData recordData = (RecordData) o;

Review Comment:
   **[Suggestion]** The ROW branch creates `new Object[arity]` and `new 
BinaryRecordDataGenerator(rowType)` (which runs a stream pipeline via 
`Arrays.stream(...).map(InternalSerializers::create)`) on every 
non-`BinaryRecordData` row write. In high-throughput pipelines with frequent 
`GenericRecordData`, this adds significant GC pressure and CPU overhead. 
Consider caching the `BinaryRecordDataGenerator` at a higher level (e.g., 
per-table) or adding a direct `GenericRecordData` write path.
   
   _— qwen3.6-plus via Qwen Code /review_



##########
flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/serializer/data/RecordDataSerializerTest.java:
##########
@@ -49,7 +69,295 @@ protected RecordData[] getTestData() {
         return new RecordData[] {
             generator.generate(new Object[] {1L, 
BinaryStringData.fromString("test1")}),
             generator.generate(new Object[] {2L, 
BinaryStringData.fromString("test2")}),
-            generator.generate(new Object[] {3L, null})
+            generator.generate(new Object[] {3L, null}),
+            GenericRecordData.of(1L, BinaryStringData.fromString("test1")),
+            GenericRecordData.of(2L, BinaryStringData.fromString("test2")),
+            GenericRecordData.of(3L, null)
         };
     }
+
+    @Test
+    void testGenericRecordDataWithVariousTypes() throws Exception {
+        RecordDataSerializer serializer = RecordDataSerializer.INSTANCE;
+

Review Comment:
   **[Suggestion]** `testGenericRecordDataWithVariousTypes` does not cover 
`DateData`, `TimeData`, or `ZonedTimestampData` field types, even though the 
serializer supports `TAG_DATE`, `TAG_TIME`, and `TAG_ZONED_TIMESTAMP`. The 
corresponding `testBinaryRecordDataWithVariousTypes` test includes all three. 
Consider adding these to ensure full type coverage for the `GenericRecordData` 
serialization path.
   
   _— qwen3.6-plus via Qwen Code /review_



##########
flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/serializer/data/GenericRecordDataSerializer.java:
##########
@@ -0,0 +1,406 @@
+/*
+ * 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.flink.cdc.runtime.serializer.data;
+
+import org.apache.flink.cdc.common.annotation.Internal;
+import org.apache.flink.cdc.common.data.ArrayData;
+import org.apache.flink.cdc.common.data.DateData;
+import org.apache.flink.cdc.common.data.DecimalData;
+import org.apache.flink.cdc.common.data.GenericArrayData;
+import org.apache.flink.cdc.common.data.GenericMapData;
+import org.apache.flink.cdc.common.data.GenericRecordData;
+import org.apache.flink.cdc.common.data.LocalZonedTimestampData;
+import org.apache.flink.cdc.common.data.MapData;
+import org.apache.flink.cdc.common.data.StringData;
+import org.apache.flink.cdc.common.data.TimeData;
+import org.apache.flink.cdc.common.data.TimestampData;
+import org.apache.flink.cdc.common.data.ZonedTimestampData;
+import org.apache.flink.cdc.common.data.binary.BinaryRecordData;
+import org.apache.flink.cdc.common.data.binary.BinaryStringData;
+import org.apache.flink.cdc.common.types.variant.BinaryVariant;
+import org.apache.flink.cdc.common.types.variant.Variant;
+import 
org.apache.flink.cdc.runtime.serializer.data.binary.BinaryRecordDataSerializer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Serializer for {@link GenericRecordData}. Uses a self-describing format 
where each field is
+ * prefixed with a type tag, so no schema information is needed at 
serialization time.
+ */
+@Internal
+public class GenericRecordDataSerializer {
+
+    // Type tags for self-describing format
+    static final byte TAG_NULL = 0;

Review Comment:
   **[Suggestion]** The TAG constants (`TAG_NULL`, `TAG_BOOLEAN`, etc.) use 
package-private visibility (`static final byte`). Since they are only consumed 
internally by `serializeField`/`deserializeField`, they should be `private 
static final byte` to match the project convention (e.g., 
`RecordDataSerializer.java` uses `private static final byte` for its type 
constants).
   
   _— qwen3.6-plus via Qwen Code /review_



##########
flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/serializer/data/RecordDataSerializer.java:
##########
@@ -33,43 +34,86 @@ public class RecordDataSerializer extends 
TypeSerializerSingleton<RecordData> {
 
     private static final long serialVersionUID = 1L;
 
+    /** Type tag for BinaryRecordData serialization. */
+    private static final byte BINARY_RECORD_TYPE = 0;
+
+    /** Type tag for GenericRecordData serialization. */
+    private static final byte GENERIC_RECORD_TYPE = 1;
+
     private final BinaryRecordDataSerializer binarySerializer = 
BinaryRecordDataSerializer.INSTANCE;
 
     public static final RecordDataSerializer INSTANCE = new 
RecordDataSerializer();
 
     @Override
     public RecordData createInstance() {
-        // BinaryRecordData is the only implementation of RecordData
         return new BinaryRecordData(1);
     }
 
     @Override
     public void serialize(RecordData recordData, DataOutputView target) throws 
IOException {
-        // BinaryRecordData is the only implementation of RecordData
-        binarySerializer.serialize((BinaryRecordData) recordData, target);
+        if (recordData instanceof BinaryRecordData) {
+            target.writeByte(BINARY_RECORD_TYPE);
+            binarySerializer.serialize((BinaryRecordData) recordData, target);
+        } else if (recordData instanceof GenericRecordData) {
+            target.writeByte(GENERIC_RECORD_TYPE);
+            GenericRecordDataSerializer.serialize((GenericRecordData) 
recordData, target);
+        } else {
+            throw new IOException(
+                    "Unsupported RecordData type: " + 
recordData.getClass().getName());
+        }
     }
 
     @Override
     public RecordData deserialize(DataInputView source) throws IOException {
-        // BinaryRecordData is the only implementation of RecordData
-        return binarySerializer.deserialize(source);
+        byte type = source.readByte();
+        if (type == BINARY_RECORD_TYPE) {
+            return binarySerializer.deserialize(source);
+        } else if (type == GENERIC_RECORD_TYPE) {
+            return GenericRecordDataSerializer.deserialize(source);
+        } else {
+            throw new IOException("Unknown RecordData type tag: " + type);
+        }
     }
 
     @Override
     public RecordData deserialize(RecordData reuse, DataInputView source) 
throws IOException {
-        return binarySerializer.deserialize((BinaryRecordData) reuse, source);
+        byte type = source.readByte();
+        if (type == BINARY_RECORD_TYPE) {
+            if (reuse instanceof BinaryRecordData) {
+                return binarySerializer.deserialize((BinaryRecordData) reuse, 
source);
+            }
+            return binarySerializer.deserialize(source);
+        } else if (type == GENERIC_RECORD_TYPE) {
+            return GenericRecordDataSerializer.deserialize(source);
+        } else {
+            throw new IOException("Unknown RecordData type tag: " + type);
+        }
     }
 
     @Override
     public RecordData copy(RecordData from) {
-        // BinaryRecordData is the only implementation of RecordData
-        return ((BinaryRecordData) from).copy();
+        if (from instanceof BinaryRecordData) {
+            return ((BinaryRecordData) from).copy();
+        } else if (from instanceof GenericRecordData) {
+            return GenericRecordDataSerializer.copy((GenericRecordData) from);
+        } else {
+            throw new RuntimeException("Unsupported RecordData type: " + 
from.getClass().getName());
+        }
     }
 
     @Override
     public RecordData copy(RecordData from, RecordData reuse) {
-        // BinaryRecordData is the only implementation of RecordData
-        return ((BinaryRecordData) from).copy((BinaryRecordData) reuse);
+        if (from instanceof BinaryRecordData) {
+            BinaryRecordData reuseRecord =
+                    (reuse instanceof BinaryRecordData)
+                            ? (BinaryRecordData) reuse
+                            : new BinaryRecordData(from.getArity());
+            return ((BinaryRecordData) from).copy(reuseRecord);
+        } else if (from instanceof GenericRecordData) {
+            return GenericRecordDataSerializer.copy((GenericRecordData) from);

Review Comment:
   **[Suggestion]** The `copy(from, reuse)` method ignores the `reuse` 
parameter for `GenericRecordData`, always delegating to 
`GenericRecordDataSerializer.copy()` which allocates a new instance. This 
differs from the `BinaryRecordData` branch which actively reuses the provided 
object. In Flink's execution model, `copy(from, reuse)` is called in hot paths 
— consistently ignoring reuse for one branch creates unnecessary GC pressure 
when `GenericRecordData` flows through the pipeline.
   
   _— qwen3.6-plus via Qwen Code /review_



##########
flink-cdc-runtime/src/main/java/org/apache/flink/cdc/runtime/serializer/data/GenericRecordDataSerializer.java:
##########
@@ -0,0 +1,406 @@
+/*
+ * 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.flink.cdc.runtime.serializer.data;
+
+import org.apache.flink.cdc.common.annotation.Internal;
+import org.apache.flink.cdc.common.data.ArrayData;
+import org.apache.flink.cdc.common.data.DateData;
+import org.apache.flink.cdc.common.data.DecimalData;
+import org.apache.flink.cdc.common.data.GenericArrayData;
+import org.apache.flink.cdc.common.data.GenericMapData;
+import org.apache.flink.cdc.common.data.GenericRecordData;
+import org.apache.flink.cdc.common.data.LocalZonedTimestampData;
+import org.apache.flink.cdc.common.data.MapData;
+import org.apache.flink.cdc.common.data.StringData;
+import org.apache.flink.cdc.common.data.TimeData;
+import org.apache.flink.cdc.common.data.TimestampData;
+import org.apache.flink.cdc.common.data.ZonedTimestampData;
+import org.apache.flink.cdc.common.data.binary.BinaryRecordData;
+import org.apache.flink.cdc.common.data.binary.BinaryStringData;
+import org.apache.flink.cdc.common.types.variant.BinaryVariant;
+import org.apache.flink.cdc.common.types.variant.Variant;
+import 
org.apache.flink.cdc.runtime.serializer.data.binary.BinaryRecordDataSerializer;
+import org.apache.flink.core.memory.DataInputView;
+import org.apache.flink.core.memory.DataOutputView;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Serializer for {@link GenericRecordData}. Uses a self-describing format 
where each field is
+ * prefixed with a type tag, so no schema information is needed at 
serialization time.
+ */
+@Internal
+public class GenericRecordDataSerializer {
+
+    // Type tags for self-describing format
+    static final byte TAG_NULL = 0;
+    static final byte TAG_BOOLEAN = 1;
+    static final byte TAG_BYTE = 2;
+    static final byte TAG_SHORT = 3;
+    static final byte TAG_INT = 4;
+    static final byte TAG_LONG = 5;
+    static final byte TAG_FLOAT = 6;
+    static final byte TAG_DOUBLE = 7;
+    static final byte TAG_STRING = 8;
+    static final byte TAG_BINARY = 9;
+    static final byte TAG_DECIMAL = 10;
+    static final byte TAG_TIMESTAMP = 11;
+    static final byte TAG_ZONED_TIMESTAMP = 12;
+    static final byte TAG_LOCAL_ZONED_TIMESTAMP = 13;
+    static final byte TAG_DATE = 14;
+    static final byte TAG_TIME = 15;
+    static final byte TAG_GENERIC_RECORD = 16;
+    static final byte TAG_BINARY_RECORD = 17;
+    static final byte TAG_ARRAY = 18;
+    static final byte TAG_MAP = 19;
+    static final byte TAG_VARIANT = 20;
+
+    private GenericRecordDataSerializer() {}
+
+    /** Serializes a {@link GenericRecordData} to the given output view. */
+    public static void serialize(GenericRecordData record, DataOutputView 
target)
+            throws IOException {
+        int arity = record.getArity();
+        target.writeInt(arity);
+        for (int i = 0; i < arity; i++) {
+            serializeField(record.getField(i), target);
+        }
+    }
+
+    /** Deserializes a {@link GenericRecordData} from the given input view. */
+    public static GenericRecordData deserialize(DataInputView source) throws 
IOException {
+        int arity = source.readInt();
+        GenericRecordData record = new GenericRecordData(arity);
+        for (int i = 0; i < arity; i++) {
+            record.setField(i, deserializeField(source));
+        }
+        return record;
+    }
+
+    /** Creates a deep copy of the given {@link GenericRecordData}. */
+    public static GenericRecordData copy(GenericRecordData from) {
+        int arity = from.getArity();
+        GenericRecordData copy = new GenericRecordData(arity);
+        for (int i = 0; i < arity; i++) {
+            copy.setField(i, copyField(from.getField(i)));
+        }
+        return copy;
+    }
+
+    // ---- Field-level serialization ----
+
+    static void serializeField(Object field, DataOutputView target) throws 
IOException {
+        if (field == null) {
+            target.writeByte(TAG_NULL);
+        } else if (field instanceof Boolean) {
+            target.writeByte(TAG_BOOLEAN);
+            target.writeBoolean((Boolean) field);
+        } else if (field instanceof Byte) {
+            target.writeByte(TAG_BYTE);
+            target.writeByte((Byte) field);
+        } else if (field instanceof Short) {
+            target.writeByte(TAG_SHORT);
+            target.writeShort((Short) field);
+        } else if (field instanceof Integer) {
+            target.writeByte(TAG_INT);
+            target.writeInt((Integer) field);
+        } else if (field instanceof Long) {
+            target.writeByte(TAG_LONG);
+            target.writeLong((Long) field);
+        } else if (field instanceof Float) {
+            target.writeByte(TAG_FLOAT);
+            target.writeFloat((Float) field);
+        } else if (field instanceof Double) {
+            target.writeByte(TAG_DOUBLE);
+            target.writeDouble((Double) field);
+        } else if (field instanceof StringData) {
+            target.writeByte(TAG_STRING);
+            byte[] bytes = ((StringData) field).toBytes();
+            target.writeInt(bytes.length);
+            target.write(bytes);
+        } else if (field instanceof byte[]) {
+            target.writeByte(TAG_BINARY);
+            byte[] bytes = (byte[]) field;
+            target.writeInt(bytes.length);
+            target.write(bytes);
+        } else if (field instanceof DecimalData) {
+            target.writeByte(TAG_DECIMAL);
+            DecimalData decimal = (DecimalData) field;
+            // Use DecimalData's precision/scale (SQL DECIMAL(p,s)) instead of 
BigDecimal's
+            target.writeInt(decimal.precision());
+            target.writeInt(decimal.scale());
+            byte[] unscaled = decimal.toUnscaledBytes();
+            target.writeInt(unscaled.length);
+            target.write(unscaled);
+        } else if (field instanceof TimestampData) {
+            target.writeByte(TAG_TIMESTAMP);
+            TimestampData ts = (TimestampData) field;
+            target.writeLong(ts.getMillisecond());
+            target.writeInt(ts.getNanoOfMillisecond());
+        } else if (field instanceof ZonedTimestampData) {
+            target.writeByte(TAG_ZONED_TIMESTAMP);
+            ZonedTimestampData zts = (ZonedTimestampData) field;
+            target.writeLong(zts.getMillisecond());
+            target.writeInt(zts.getNanoOfMillisecond());
+            byte[] zoneBytes = 
zts.getZoneId().getBytes(StandardCharsets.UTF_8);
+            target.writeInt(zoneBytes.length);
+            target.write(zoneBytes);
+        } else if (field instanceof LocalZonedTimestampData) {
+            target.writeByte(TAG_LOCAL_ZONED_TIMESTAMP);
+            LocalZonedTimestampData lzts = (LocalZonedTimestampData) field;
+            target.writeLong(lzts.getEpochMillisecond());
+            target.writeInt(lzts.getEpochNanoOfMillisecond());
+        } else if (field instanceof DateData) {
+            target.writeByte(TAG_DATE);
+            target.writeInt(((DateData) field).toEpochDay());
+        } else if (field instanceof TimeData) {
+            target.writeByte(TAG_TIME);
+            target.writeInt(((TimeData) field).toMillisOfDay());
+        } else if (field instanceof GenericRecordData) {
+            target.writeByte(TAG_GENERIC_RECORD);
+            serialize((GenericRecordData) field, target);
+        } else if (field instanceof BinaryRecordData) {
+            target.writeByte(TAG_BINARY_RECORD);
+            BinaryRecordDataSerializer.INSTANCE.serialize((BinaryRecordData) 
field, target);
+        } else if (field instanceof ArrayData) {
+            target.writeByte(TAG_ARRAY);
+            serializeArrayData((ArrayData) field, target);
+        } else if (field instanceof MapData) {
+            target.writeByte(TAG_MAP);
+            serializeMapData((MapData) field, target);
+        } else if (field instanceof Variant) {
+            target.writeByte(TAG_VARIANT);
+            serializeVariant((Variant) field, target);
+        } else {
+            throw new IOException(
+                    "Unsupported field type in GenericRecordData: " + 
field.getClass().getName());
+        }
+    }
+
+    static Object deserializeField(DataInputView source) throws IOException {
+        byte tag = source.readByte();
+        switch (tag) {
+            case TAG_NULL:
+                return null;
+            case TAG_BOOLEAN:
+                return source.readBoolean();
+            case TAG_BYTE:
+                return source.readByte();
+            case TAG_SHORT:
+                return source.readShort();
+            case TAG_INT:
+                return source.readInt();
+            case TAG_LONG:
+                return source.readLong();
+            case TAG_FLOAT:
+                return source.readFloat();
+            case TAG_DOUBLE:
+                return source.readDouble();
+            case TAG_STRING:
+                {
+                    int len = source.readInt();
+                    byte[] bytes = new byte[len];
+                    source.readFully(bytes);
+                    return BinaryStringData.fromBytes(bytes);
+                }
+            case TAG_BINARY:
+                {
+                    int len = source.readInt();
+                    byte[] bytes = new byte[len];
+                    source.readFully(bytes);
+                    return bytes;
+                }
+            case TAG_DECIMAL:
+                {
+                    int precision = source.readInt();
+                    int scale = source.readInt();
+                    int len = source.readInt();
+                    byte[] unscaled = new byte[len];
+                    source.readFully(unscaled);
+                    return DecimalData.fromUnscaledBytes(unscaled, precision, 
scale);
+                }
+            case TAG_TIMESTAMP:
+                return TimestampData.fromMillis(source.readLong(), 
source.readInt());
+            case TAG_ZONED_TIMESTAMP:
+                {
+                    long millis = source.readLong();
+                    int nanos = source.readInt();
+                    int zoneLen = source.readInt();
+                    byte[] zoneBytes = new byte[zoneLen];
+                    source.readFully(zoneBytes);
+                    return ZonedTimestampData.of(
+                            millis, nanos, new String(zoneBytes, 
StandardCharsets.UTF_8));
+                }
+            case TAG_LOCAL_ZONED_TIMESTAMP:
+                return 
LocalZonedTimestampData.fromEpochMillis(source.readLong(), source.readInt());
+            case TAG_DATE:
+                return DateData.fromEpochDay(source.readInt());
+            case TAG_TIME:
+                return TimeData.fromMillisOfDay(source.readInt());
+            case TAG_GENERIC_RECORD:
+                return deserialize(source);
+            case TAG_BINARY_RECORD:
+                return BinaryRecordDataSerializer.INSTANCE.deserialize(source);
+            case TAG_ARRAY:
+                return deserializeArrayData(source);
+            case TAG_MAP:
+                return deserializeMapData(source);
+            case TAG_VARIANT:
+                return deserializeVariant(source);
+            default:
+                throw new IOException("Unknown field type tag: " + tag);
+        }
+    }
+
+    // ---- ArrayData serialization ----
+
+    private static void serializeArrayData(ArrayData arrayData, DataOutputView 
target)
+            throws IOException {
+        if (arrayData instanceof GenericArrayData) {
+            Object[] elements = ((GenericArrayData) arrayData).toObjectArray();
+            target.writeInt(elements.length);
+            for (Object element : elements) {
+                serializeField(element, target);
+            }
+        } else {
+            throw new IOException(
+                    "Serialization of non-generic ArrayData is not supported 
in GenericRecordDataSerializer. "
+                            + "Actual type: "
+                            + arrayData.getClass().getName());
+        }
+    }
+
+    private static ArrayData deserializeArrayData(DataInputView source) throws 
IOException {
+        int size = source.readInt();
+        Object[] elements = new Object[size];
+        for (int i = 0; i < size; i++) {
+            elements[i] = deserializeField(source);
+        }
+        return new GenericArrayData(elements);
+    }
+
+    // ---- MapData serialization ----
+
+    private static void serializeMapData(MapData mapData, DataOutputView 
target)
+            throws IOException {
+        if (mapData instanceof GenericMapData) {
+            ArrayData keyArray = mapData.keyArray();
+            ArrayData valueArray = mapData.valueArray();
+            if (!(keyArray instanceof GenericArrayData)
+                    || !(valueArray instanceof GenericArrayData)) {
+                throw new IOException(
+                        "MapData with non-generic key/value arrays is not 
supported in GenericRecordDataSerializer.");
+            }
+            int size = mapData.size();
+            target.writeInt(size);
+            Object[] keys = ((GenericArrayData) keyArray).toObjectArray();
+            Object[] values = ((GenericArrayData) valueArray).toObjectArray();
+            for (int i = 0; i < size; i++) {
+                serializeField(keys[i], target);
+                serializeField(values[i], target);
+            }
+        } else {
+            throw new IOException(
+                    "Serialization of non-generic MapData is not supported in 
GenericRecordDataSerializer. "
+                            + "Actual type: "
+                            + mapData.getClass().getName());
+        }
+    }
+
+    private static MapData deserializeMapData(DataInputView source) throws 
IOException {
+        int size = source.readInt();
+        Map<Object, Object> map = new LinkedHashMap<>(size);
+        for (int i = 0; i < size; i++) {
+            Object key = deserializeField(source);
+            Object value = deserializeField(source);
+            map.put(key, value);
+        }
+        return new GenericMapData(map);
+    }
+
+    // ---- Variant serialization ----
+
+    private static void serializeVariant(Variant variant, DataOutputView 
target)
+            throws IOException {
+        if (variant instanceof BinaryVariant) {
+            BinaryVariant bv = (BinaryVariant) variant;
+            byte[] value = bv.getValue();
+            byte[] metadata = bv.getMetadata();
+            target.writeInt(value.length);
+            target.write(value);
+            target.writeInt(metadata.length);
+            target.write(metadata);
+        } else {
+            throw new IOException("Unsupported Variant type: " + 
variant.getClass().getName());
+        }
+    }
+
+    private static Variant deserializeVariant(DataInputView source) throws 
IOException {
+        int valueLen = source.readInt();
+        byte[] value = new byte[valueLen];
+        source.readFully(value);
+        int metadataLen = source.readInt();
+        byte[] metadata = new byte[metadataLen];
+        source.readFully(metadata);
+        return new BinaryVariant(value, metadata);
+    }
+
+    // ---- Field copy ----
+
+    private static Object copyField(Object field) {
+        if (field == null) {
+            return null;
+        }
+        // Most CDC internal data types are immutable, so shallow copy is safe
+        if (field instanceof byte[]) {
+            return ((byte[]) field).clone();
+        } else if (field instanceof GenericRecordData) {
+            return copy((GenericRecordData) field);
+        } else if (field instanceof BinaryRecordData) {
+            return ((BinaryRecordData) field).copy();
+        } else if (field instanceof GenericArrayData) {
+            Object[] elements = ((GenericArrayData) field).toObjectArray();
+            Object[] copied = new Object[elements.length];
+            for (int i = 0; i < elements.length; i++) {
+                copied[i] = copyField(elements[i]);
+            }
+            return new GenericArrayData(copied);
+        } else if (field instanceof GenericMapData) {
+            GenericMapData mapData = (GenericMapData) field;
+            ArrayData keyArray = mapData.keyArray();
+            ArrayData valueArray = mapData.valueArray();
+            if (keyArray instanceof GenericArrayData && valueArray instanceof 
GenericArrayData) {
+                Object[] keys = ((GenericArrayData) keyArray).toObjectArray();
+                Object[] values = ((GenericArrayData) 
valueArray).toObjectArray();
+                Map<Object, Object> newMap = new LinkedHashMap<>(keys.length);
+                for (int i = 0; i < keys.length; i++) {
+                    newMap.put(copyField(keys[i]), copyField(values[i]));
+                }
+                return new GenericMapData(newMap);
+            }

Review Comment:
   **[Suggestion]** The `return field;` fallback here is dead code. 
`GenericMapData.keyArray()` always returns `new 
GenericArrayData(map.keySet().toArray())` and `valueArray()` always returns 
`new GenericArrayData(map.values().toArray())`, so the `instanceof 
GenericArrayData` check is always true. Consider replacing the fallback with 
`throw new IllegalStateException(...)` to make the invariant explicit and 
fail-fast if the assumption ever breaks.
   
   _— qwen3.6-plus via Qwen Code /review_



##########
flink-cdc-runtime/src/test/java/org/apache/flink/cdc/runtime/serializer/data/RecordDataSerializerTest.java:
##########
@@ -49,7 +69,295 @@ protected RecordData[] getTestData() {
         return new RecordData[] {
             generator.generate(new Object[] {1L, 
BinaryStringData.fromString("test1")}),
             generator.generate(new Object[] {2L, 
BinaryStringData.fromString("test2")}),
-            generator.generate(new Object[] {3L, null})
+            generator.generate(new Object[] {3L, null}),
+            GenericRecordData.of(1L, BinaryStringData.fromString("test1")),
+            GenericRecordData.of(2L, BinaryStringData.fromString("test2")),
+            GenericRecordData.of(3L, null)
         };
     }
+
+    @Test
+    void testGenericRecordDataWithVariousTypes() throws Exception {
+        RecordDataSerializer serializer = RecordDataSerializer.INSTANCE;
+
+        GenericRecordData record =
+                GenericRecordData.of(
+                        true,
+                        (byte) 42,
+                        (short) 1024,
+                        123456,
+                        789L,
+                        3.14f,
+                        2.718281828,
+                        BinaryStringData.fromString("hello"),
+                        new byte[] {1, 2, 3},
+                        DecimalData.fromBigDecimal(new 
BigDecimal("12345.6789"), 10, 4),
+                        TimestampData.fromMillis(1609459200000L, 123456),
+                        
LocalZonedTimestampData.fromEpochMillis(1609459200000L, 654321),
+                        null);
+
+        DataOutputSerializer out = new DataOutputSerializer(256);
+        serializer.serialize(record, out);
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        RecordData deserialized = serializer.deserialize(in);
+
+        assertThat(deserialized).isInstanceOf(GenericRecordData.class);
+        assertThat(deserialized.getArity()).isEqualTo(record.getArity());
+        assertThat(deserialized.getBoolean(0)).isTrue();
+        assertThat(deserialized.getByte(1)).isEqualTo((byte) 42);
+        assertThat(deserialized.getShort(2)).isEqualTo((short) 1024);
+        assertThat(deserialized.getInt(3)).isEqualTo(123456);
+        assertThat(deserialized.getLong(4)).isEqualTo(789L);
+        assertThat(deserialized.getFloat(5)).isEqualTo(3.14f);
+        assertThat(deserialized.getDouble(6)).isEqualTo(2.718281828);
+        assertThat(deserialized.getString(7).toString()).isEqualTo("hello");
+        assertThat(deserialized.getBinary(8)).isEqualTo(new byte[] {1, 2, 3});
+        assertThat(deserialized.getDecimal(9, 10, 4).toBigDecimal())
+                .isEqualByComparingTo(new BigDecimal("12345.6789"));
+        assertThat(deserialized.getTimestamp(10, 
6).getMillisecond()).isEqualTo(1609459200000L);
+        assertThat(deserialized.getTimestamp(10, 
6).getNanoOfMillisecond()).isEqualTo(123456);
+        assertThat(deserialized.getLocalZonedTimestampData(11, 
6).getEpochMillisecond())
+                .isEqualTo(1609459200000L);
+        assertThat(deserialized.isNullAt(12)).isTrue();
+    }
+
+    @Test
+    void testBinaryRecordDataWithVariousTypes() throws Exception {
+        RecordDataSerializer serializer = RecordDataSerializer.INSTANCE;
+
+        RowType rowType =
+                RowType.of(
+                        DataTypes.BOOLEAN(),
+                        DataTypes.TINYINT(),
+                        DataTypes.SMALLINT(),
+                        DataTypes.INT(),
+                        DataTypes.BIGINT(),
+                        DataTypes.FLOAT(),
+                        DataTypes.DOUBLE(),
+                        DataTypes.STRING(),
+                        DataTypes.BYTES(),
+                        DataTypes.DECIMAL(10, 4),
+                        DataTypes.TIMESTAMP(6),
+                        DataTypes.TIMESTAMP_LTZ(6),
+                        DataTypes.TIMESTAMP_TZ(6),
+                        DataTypes.DATE(),
+                        DataTypes.TIME(),
+                        DataTypes.STRING());
+
+        BinaryRecordData record =
+                new BinaryRecordDataGenerator(rowType)
+                        .generate(
+                                new Object[] {
+                                    true,
+                                    (byte) 42,
+                                    (short) 1024,
+                                    123456,
+                                    789L,
+                                    3.14f,
+                                    2.718281828,
+                                    BinaryStringData.fromString("hello"),
+                                    new byte[] {1, 2, 3},
+                                    DecimalData.fromBigDecimal(new 
BigDecimal("12345.6789"), 10, 4),
+                                    TimestampData.fromMillis(1609459200000L, 
123456),
+                                    
LocalZonedTimestampData.fromEpochMillis(1609459200000L, 654321),
+                                    ZonedTimestampData.of(1609459200000L, 
789012, "UTC"),
+                                    DateData.fromEpochDay(18628),
+                                    TimeData.fromMillisOfDay(43200000),
+                                    null
+                                });
+
+        DataOutputSerializer out = new DataOutputSerializer(256);
+        serializer.serialize(record, out);
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        RecordData deserialized = serializer.deserialize(in);
+
+        assertThat(deserialized).isInstanceOf(BinaryRecordData.class);
+        assertThat(deserialized.getArity()).isEqualTo(record.getArity());
+        assertThat(deserialized.getBoolean(0)).isTrue();
+        assertThat(deserialized.getByte(1)).isEqualTo((byte) 42);
+        assertThat(deserialized.getShort(2)).isEqualTo((short) 1024);
+        assertThat(deserialized.getInt(3)).isEqualTo(123456);
+        assertThat(deserialized.getLong(4)).isEqualTo(789L);
+        assertThat(deserialized.getFloat(5)).isEqualTo(3.14f);
+        assertThat(deserialized.getDouble(6)).isEqualTo(2.718281828);
+        assertThat(deserialized.getString(7).toString()).isEqualTo("hello");
+        assertThat(deserialized.getBinary(8)).isEqualTo(new byte[] {1, 2, 3});
+        assertThat(deserialized.getDecimal(9, 10, 4).toBigDecimal())
+                .isEqualByComparingTo(new BigDecimal("12345.6789"));
+        assertThat(deserialized.getTimestamp(10, 
6).getMillisecond()).isEqualTo(1609459200000L);
+        assertThat(deserialized.getTimestamp(10, 
6).getNanoOfMillisecond()).isEqualTo(123456);
+        assertThat(deserialized.getLocalZonedTimestampData(11, 
6).getEpochMillisecond())
+                .isEqualTo(1609459200000L);
+        assertThat(deserialized.getZonedTimestamp(12, 6).getMillisecond())
+                .isEqualTo(1609459200000L);
+        assertThat(deserialized.getDate(13).toEpochDay()).isEqualTo(18628);
+        
assertThat(deserialized.getTime(14).toMillisOfDay()).isEqualTo(43200000);
+        assertThat(deserialized.isNullAt(15)).isTrue();
+    }
+
+    @Test
+    void testGenericRecordDataWithNestedTypes() throws Exception {
+        RecordDataSerializer serializer = RecordDataSerializer.INSTANCE;
+
+        GenericRecordData nestedGeneric =
+                GenericRecordData.of(42, 
BinaryStringData.fromString("nested"));
+
+        BinaryRecordData nestedBinary =
+                new BinaryRecordDataGenerator(RowType.of(DataTypes.INT(), 
DataTypes.STRING()))
+                        .generate(new Object[] {99, 
BinaryStringData.fromString("binary-nested")});
+
+        GenericArrayData intArray = new GenericArrayData(new int[] {1, 2, 3, 
4, 5});
+        GenericArrayData stringArray =
+                new GenericArrayData(
+                        new Object[] {
+                            BinaryStringData.fromString("a"), 
BinaryStringData.fromString("b")
+                        });
+        GenericMapData map =
+                new GenericMapData(
+                        Map.of(
+                                BinaryStringData.fromString("k1"),
+                                100,
+                                BinaryStringData.fromString("k2"),
+                                200));
+
+        GenericRecordData record =
+                GenericRecordData.of(nestedGeneric, nestedBinary, intArray, 
stringArray, map);
+
+        DataOutputSerializer out = new DataOutputSerializer(512);
+        serializer.serialize(record, out);
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        RecordData deserialized = serializer.deserialize(in);
+
+        assertThat(deserialized).isInstanceOf(GenericRecordData.class);
+        assertThat(deserialized.getArity()).isEqualTo(5);
+
+        RecordData dNestedGeneric = deserialized.getRow(0, 2);
+        assertThat(dNestedGeneric.getInt(0)).isEqualTo(42);
+        assertThat(dNestedGeneric.getString(1).toString()).isEqualTo("nested");
+
+        RecordData dNestedBinary = deserialized.getRow(1, 2);
+        assertThat(dNestedBinary.getInt(0)).isEqualTo(99);
+        
assertThat(dNestedBinary.getString(1).toString()).isEqualTo("binary-nested");
+
+        ArrayData dIntArray = deserialized.getArray(2);
+        assertThat(dIntArray.size()).isEqualTo(5);
+        assertThat(dIntArray.getInt(0)).isEqualTo(1);
+        assertThat(dIntArray.getInt(4)).isEqualTo(5);
+
+        ArrayData dStringArray = deserialized.getArray(3);
+        assertThat(dStringArray.size()).isEqualTo(2);
+        assertThat(dStringArray.getString(0).toString()).isEqualTo("a");
+        assertThat(dStringArray.getString(1).toString()).isEqualTo("b");
+
+        MapData dMap = deserialized.getMap(4);

Review Comment:
   **[Suggestion]** `testGenericRecordDataWithNestedTypes` checks `dMap.size() 
== 2` but does not verify actual key-value content (e.g., `"k1"→100`, 
`"k2"→200`). If map key-value pairs were corrupted or mismatched during 
serialization/deserialization, this test would not catch it. Consider adding 
assertions like 
`assertThat(dMap.get(BinaryStringData.fromString("k1"))).isEqualTo(100)`.
   
   _— qwen3.6-plus via Qwen Code /review_



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to