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

gyfora pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git


The following commit(s) were added to refs/heads/master by this push:
     new c830df80ee5 [FLINK-40120][table] Persist RowData field names in 
RowDataSerializerSnapshot (#28717)
c830df80ee5 is described below

commit c830df80ee5723cd05bd6cba3e8a384faa8d022e
Author: Weiqing Yang <[email protected]>
AuthorDate: Fri Jul 17 00:38:20 2026 -0700

    [FLINK-40120][table] Persist RowData field names in 
RowDataSerializerSnapshot (#28717)
---
 .../runtime/typeutils/InternalSerializers.java     |   1 +
 .../table/runtime/typeutils/RowDataSerializer.java |  55 +++++-
 .../typeutils/RowDataSerializerFieldNamesTest.java | 197 +++++++++++++++++++++
 3 files changed, 247 insertions(+), 6 deletions(-)

diff --git 
a/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/InternalSerializers.java
 
b/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/InternalSerializers.java
index 371dcd136e1..4334591cbd1 100644
--- 
a/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/InternalSerializers.java
+++ 
b/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/InternalSerializers.java
@@ -108,6 +108,7 @@ public final class InternalSerializers {
                 MapType mapType = (MapType) type;
                 return new MapDataSerializer(mapType.getKeyType(), 
mapType.getValueType());
             case ROW:
+                return new RowDataSerializer((RowType) type);
             case STRUCTURED_TYPE:
                 return new RowDataSerializer(type.getChildren().toArray(new 
LogicalType[0]));
             case DISTINCT_TYPE:
diff --git 
a/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/RowDataSerializer.java
 
b/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/RowDataSerializer.java
index 7a634f67efe..718fbc20be9 100644
--- 
a/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/RowDataSerializer.java
+++ 
b/flink-table/flink-table-type-utils/src/main/java/org/apache/flink/table/runtime/typeutils/RowDataSerializer.java
@@ -19,6 +19,7 @@
 package org.apache.flink.table.runtime.typeutils;
 
 import org.apache.flink.annotation.Internal;
+import org.apache.flink.annotation.VisibleForTesting;
 import org.apache.flink.api.common.typeutils.CompositeTypeSerializerUtil;
 import org.apache.flink.api.common.typeutils.NestedSerializersSnapshotDelegate;
 import org.apache.flink.api.common.typeutils.TypeSerializer;
@@ -40,6 +41,8 @@ import org.apache.flink.table.types.logical.LogicalType;
 import org.apache.flink.table.types.logical.RowType;
 import org.apache.flink.util.InstantiationUtil;
 
+import javax.annotation.Nullable;
+
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.stream.IntStream;
@@ -51,6 +54,7 @@ public class RowDataSerializer extends 
AbstractRowDataSerializer<RowData> {
 
     private BinaryRowDataSerializer binarySerializer;
     private final LogicalType[] types;
+    private final @Nullable String[] fieldNames;
     private final TypeSerializer[] fieldSerializers;
     private final RowData.FieldGetter[] fieldGetters;
 
@@ -62,7 +66,8 @@ public class RowDataSerializer extends 
AbstractRowDataSerializer<RowData> {
                 rowType.getChildren().toArray(new LogicalType[0]),
                 rowType.getChildren().stream()
                         .map(InternalSerializers::create)
-                        .toArray(TypeSerializer[]::new));
+                        .toArray(TypeSerializer[]::new),
+                rowType.getFieldNames().toArray(new String[0]));
     }
 
     public RowDataSerializer(LogicalType... types) {
@@ -74,7 +79,15 @@ public class RowDataSerializer extends 
AbstractRowDataSerializer<RowData> {
     }
 
     public RowDataSerializer(LogicalType[] types, TypeSerializer<?>[] 
fieldSerializers) {
+        this(types, fieldSerializers, null);
+    }
+
+    private RowDataSerializer(
+            LogicalType[] types,
+            TypeSerializer<?>[] fieldSerializers,
+            @Nullable String[] fieldNames) {
         this.types = types;
+        this.fieldNames = fieldNames;
         this.fieldSerializers = fieldSerializers;
         this.binarySerializer = new BinaryRowDataSerializer(types.length);
         this.fieldGetters =
@@ -83,13 +96,24 @@ public class RowDataSerializer extends 
AbstractRowDataSerializer<RowData> {
                         .toArray(RowData.FieldGetter[]::new);
     }
 
+    @VisibleForTesting
+    @Nullable
+    String[] getFieldNames() {
+        return fieldNames;
+    }
+
+    @VisibleForTesting
+    TypeSerializer[] fieldSerializers() {
+        return fieldSerializers;
+    }
+
     @Override
     public TypeSerializer<RowData> duplicate() {
         TypeSerializer<?>[] duplicateFieldSerializers = new 
TypeSerializer[fieldSerializers.length];
         for (int i = 0; i < fieldSerializers.length; i++) {
             duplicateFieldSerializers[i] = fieldSerializers[i].duplicate();
         }
-        return new RowDataSerializer(types, duplicateFieldSerializers);
+        return new RowDataSerializer(types, duplicateFieldSerializers, 
fieldNames);
     }
 
     @Override
@@ -274,14 +298,15 @@ public class RowDataSerializer extends 
AbstractRowDataSerializer<RowData> {
 
     @Override
     public TypeSerializerSnapshot<RowData> snapshotConfiguration() {
-        return new RowDataSerializerSnapshot(types, fieldSerializers);
+        return new RowDataSerializerSnapshot(types, fieldSerializers, 
fieldNames);
     }
 
     /** {@link TypeSerializerSnapshot} for {@link BinaryRowDataSerializer}. */
     public static final class RowDataSerializerSnapshot implements 
TypeSerializerSnapshot<RowData> {
-        private static final int CURRENT_VERSION = 3;
+        private static final int CURRENT_VERSION = 4;
 
         private LogicalType[] types;
+        private @Nullable String[] fieldNames;
         private NestedSerializersSnapshotDelegate 
nestedSerializersSnapshotDelegate;
 
         @SuppressWarnings("unused")
@@ -289,8 +314,10 @@ public class RowDataSerializer extends 
AbstractRowDataSerializer<RowData> {
             // this constructor is used when restoring from a 
checkpoint/savepoint.
         }
 
-        RowDataSerializerSnapshot(LogicalType[] types, TypeSerializer[] 
serializers) {
+        RowDataSerializerSnapshot(
+                LogicalType[] types, TypeSerializer[] serializers, @Nullable 
String[] fieldNames) {
             this.types = types;
+            this.fieldNames = fieldNames;
             this.nestedSerializersSnapshotDelegate =
                     new NestedSerializersSnapshotDelegate(serializers);
         }
@@ -308,6 +335,14 @@ public class RowDataSerializer extends 
AbstractRowDataSerializer<RowData> {
                 InstantiationUtil.serializeObject(stream, previousType);
             }
             
nestedSerializersSnapshotDelegate.writeNestedSerializerSnapshots(out);
+            // A false flag means no field names (null); the count is not 
stored because it always
+            // equals the number of fields.
+            out.writeBoolean(fieldNames != null);
+            if (fieldNames != null) {
+                for (String fieldName : fieldNames) {
+                    out.writeUTF(fieldName);
+                }
+            }
         }
 
         @Override
@@ -327,12 +362,20 @@ public class RowDataSerializer extends 
AbstractRowDataSerializer<RowData> {
             this.nestedSerializersSnapshotDelegate =
                     
NestedSerializersSnapshotDelegate.readNestedSerializerSnapshots(
                             in, userCodeClassLoader);
+            if (readVersion >= 4 && in.readBoolean()) {
+                fieldNames = new String[types.length];
+                for (int i = 0; i < types.length; i++) {
+                    fieldNames[i] = in.readUTF();
+                }
+            }
         }
 
         @Override
         public RowDataSerializer restoreSerializer() {
             return new RowDataSerializer(
-                    types, 
nestedSerializersSnapshotDelegate.getRestoredNestedSerializers());
+                    types,
+                    
nestedSerializersSnapshotDelegate.getRestoredNestedSerializers(),
+                    fieldNames);
         }
 
         @Override
diff --git 
a/flink-table/flink-table-type-utils/src/test/java/org/apache/flink/table/runtime/typeutils/RowDataSerializerFieldNamesTest.java
 
b/flink-table/flink-table-type-utils/src/test/java/org/apache/flink/table/runtime/typeutils/RowDataSerializerFieldNamesTest.java
new file mode 100644
index 00000000000..7f76786c9a2
--- /dev/null
+++ 
b/flink-table/flink-table-type-utils/src/test/java/org/apache/flink/table/runtime/typeutils/RowDataSerializerFieldNamesTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.table.runtime.typeutils;
+
+import org.apache.flink.api.common.typeutils.NestedSerializersSnapshotDelegate;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.common.typeutils.TypeSerializerSchemaCompatibility;
+import org.apache.flink.api.common.typeutils.TypeSerializerSnapshot;
+import org.apache.flink.api.java.typeutils.runtime.DataOutputViewStream;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputSerializer;
+import org.apache.flink.table.data.GenericRowData;
+import org.apache.flink.table.data.RowData;
+import 
org.apache.flink.table.runtime.typeutils.RowDataSerializer.RowDataSerializerSnapshot;
+import org.apache.flink.table.types.logical.BigIntType;
+import org.apache.flink.table.types.logical.IntType;
+import org.apache.flink.table.types.logical.LogicalType;
+import org.apache.flink.table.types.logical.RowType;
+import org.apache.flink.table.types.logical.VarCharType;
+import org.apache.flink.util.InstantiationUtil;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for the top-level field names carried by {@link RowDataSerializer} 
and the V4 {@link
+ * RowDataSerializerSnapshot} format that persists them unconditionally. These 
assert metadata
+ * (field names, snapshot version, V3 backward compatibility, compatibility 
invariance) rather than
+ * per-record serialization round trips, which are covered by {@link 
RowDataSerializerTest}.
+ */
+class RowDataSerializerFieldNamesTest {
+
+    private static RowType rowType(String[] names, LogicalType... types) {
+        return RowType.of(types, names);
+    }
+
+    @Test
+    void internalSerializersCreateCarriesTopLevelNames() {
+        RowType rowType = rowType(new String[] {"f0", "f1"}, new IntType(), 
new BigIntType());
+
+        
assertThat(InternalSerializers.create(rowType).getFieldNames()).containsExactly("f0",
 "f1");
+    }
+
+    @Test
+    void typesOnlyConstructorsLeaveNamesNull() {
+        assertThat(new RowDataSerializer(new IntType(), new 
BigIntType()).getFieldNames()).isNull();
+        assertThat(
+                        new RowDataSerializer(
+                                        new LogicalType[] {new IntType(), new 
BigIntType()},
+                                        new TypeSerializer[] {
+                                            InternalSerializers.create(new 
IntType()),
+                                            InternalSerializers.create(new 
BigIntType())
+                                        })
+                                .getFieldNames())
+                .isNull();
+    }
+
+    @Test
+    void duplicatePreservesNames() {
+        RowType rowType = rowType(new String[] {"f0", "f1"}, new IntType(), 
new BigIntType());
+        RowDataSerializer duplicate =
+                (RowDataSerializer) 
InternalSerializers.create(rowType).duplicate();
+
+        assertThat(duplicate.getFieldNames()).containsExactly("f0", "f1");
+    }
+
+    @Test
+    void snapshotVersionIsFour() {
+        RowType rowType = rowType(new String[] {"f0", "f1"}, new IntType(), 
new BigIntType());
+
+        
assertThat(InternalSerializers.create(rowType).snapshotConfiguration().getCurrentVersion())
+                .isEqualTo(4);
+    }
+
+    @Test
+    void namesSurviveSnapshotRoundTripIncludingNestedRow() throws IOException {
+        RowType nested = rowType(new String[] {"a", "b"}, new IntType(), 
VarCharType.STRING_TYPE);
+        RowType rowType = rowType(new String[] {"outer", "tail"}, nested, new 
IntType());
+
+        RowDataSerializer restored =
+                (RowDataSerializer)
+                        
roundTrip(InternalSerializers.create(rowType).snapshotConfiguration())
+                                .restoreSerializer();
+
+        assertThat(restored.getFieldNames()).containsExactly("outer", "tail");
+        TypeSerializer<?> nestedChild = restored.fieldSerializers()[0];
+        assertThat(nestedChild).isInstanceOf(RowDataSerializer.class);
+        assertThat(((RowDataSerializer) 
nestedChild).getFieldNames()).containsExactly("a", "b");
+    }
+
+    @Test
+    void readsV3SnapshotWithoutFieldNames() throws IOException {
+        LogicalType[] types = {new IntType(), new BigIntType()};
+        TypeSerializer<?>[] fieldSerializers = {
+            InternalSerializers.create(types[0]), 
InternalSerializers.create(types[1])
+        };
+
+        // Hand-craft a genuine V3 stream: versioned header + V3 body, where 
the V3 body is the
+        // types section followed directly by the nested-delegate section, 
with no names block.
+        DataOutputSerializer out = new DataOutputSerializer(128);
+        out.writeUTF(RowDataSerializerSnapshot.class.getName());
+        out.writeInt(3);
+        out.writeInt(types.length);
+        DataOutputViewStream stream = new DataOutputViewStream(out);
+        for (LogicalType type : types) {
+            InstantiationUtil.serializeObject(stream, type);
+        }
+        new 
NestedSerializersSnapshotDelegate(fieldSerializers).writeNestedSerializerSnapshots(out);
+
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        TypeSerializerSnapshot<RowData> restored =
+                TypeSerializerSnapshot.readVersionedSnapshot(in, 
getClass().getClassLoader());
+
+        RowDataSerializer serializer = (RowDataSerializer) 
restored.restoreSerializer();
+        assertThat(serializer.getFieldNames()).isNull();
+
+        // The nested delegate was read at the correct offset (no phantom 
names consumed): the
+        // restored serializer round-trips a sample row.
+        GenericRowData sample = GenericRowData.of(7, 11L);
+        DataOutputSerializer rowOut = new DataOutputSerializer(32);
+        serializer.serialize(sample, rowOut);
+        RowData deserialized =
+                serializer.deserialize(new 
DataInputDeserializer(rowOut.getCopyOfBuffer()));
+        assertThat(deserialized.getInt(0)).isEqualTo(7);
+        assertThat(deserialized.getLong(1)).isEqualTo(11L);
+    }
+
+    @Test
+    void equalsAndHashCodeIgnoreFieldNames() {
+        RowDataSerializer named =
+                InternalSerializers.create(
+                        rowType(new String[] {"f0", "f1"}, new IntType(), new 
BigIntType()));
+        RowDataSerializer differentlyNamed =
+                InternalSerializers.create(
+                        rowType(new String[] {"x", "y"}, new IntType(), new 
BigIntType()));
+
+        assertThat(named).isEqualTo(differentlyNamed);
+        assertThat(named.hashCode()).isEqualTo(differentlyNamed.hashCode());
+    }
+
+    @Test
+    void differentFieldNamesRemainCompatibleWhenTypesMatch() {
+        TypeSerializerSchemaCompatibility<RowData> compatibility =
+                resolve(
+                        rowType(new String[] {"f0", "f1"}, new IntType(), new 
BigIntType()),
+                        rowType(new String[] {"x", "y"}, new IntType(), new 
BigIntType()));
+
+        assertThat(compatibility.isCompatibleAsIs()).isTrue();
+    }
+
+    @Test
+    void differentTypesRemainIncompatibleRegardlessOfNames() {
+        TypeSerializerSchemaCompatibility<RowData> compatibility =
+                resolve(
+                        rowType(new String[] {"f0", "f1"}, new IntType(), new 
BigIntType()),
+                        rowType(new String[] {"f0", "f1"}, new IntType(), new 
IntType()));
+
+        assertThat(compatibility.isIncompatible()).isTrue();
+    }
+
+    private static TypeSerializerSchemaCompatibility<RowData> resolve(
+            RowType newType, RowType oldType) {
+        TypeSerializerSnapshot<RowData> newSnapshot =
+                InternalSerializers.create(newType).snapshotConfiguration();
+        TypeSerializerSnapshot<RowData> oldSnapshot =
+                InternalSerializers.create(oldType).snapshotConfiguration();
+        return newSnapshot.resolveSchemaCompatibility(oldSnapshot);
+    }
+
+    private static TypeSerializerSnapshot<RowData> roundTrip(
+            TypeSerializerSnapshot<RowData> snapshot) throws IOException {
+        DataOutputSerializer out = new DataOutputSerializer(128);
+        TypeSerializerSnapshot.writeVersionedSnapshot(out, snapshot);
+        DataInputDeserializer in = new 
DataInputDeserializer(out.getCopyOfBuffer());
+        return TypeSerializerSnapshot.readVersionedSnapshot(
+                in, RowDataSerializerFieldNamesTest.class.getClassLoader());
+    }
+}

Reply via email to