This is an automated email from the ASF dual-hosted git repository.
dianfu pushed a commit to branch release-1.20
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/release-1.20 by this push:
new 770a086545b [FLINK-41228][python] Fix thread-mode conversion of
arrays, lists and maps (#29630)
770a086545b is described below
commit 770a086545bd8218b2d4c7ffb91e19740a224655
Author: Sangkyoon Nam <[email protected]>
AuthorDate: Fri Oct 30 15:38:47 2026 +0900
[FLINK-41228][python] Fix thread-mode conversion of arrays, lists and maps
(#29630)
---
.../pyflink/datastream/tests/test_data_stream.py | 27 ++++++
.../pyflink/fn_execution/embedded/converters.py | 8 +-
.../flink/streaming/api/utils/PythonTypeUtils.java | 98 +++++++++++++++-------
.../streaming/api/utils/PythonTypeUtilsTest.java | 59 +++++++++++++
4 files changed, 159 insertions(+), 33 deletions(-)
diff --git a/flink-python/pyflink/datastream/tests/test_data_stream.py
b/flink-python/pyflink/datastream/tests/test_data_stream.py
index 4b41ee6323b..17d0a0650a1 100644
--- a/flink-python/pyflink/datastream/tests/test_data_stream.py
+++ b/flink-python/pyflink/datastream/tests/test_data_stream.py
@@ -1347,6 +1347,33 @@ class
EmbeddedDataStreamStreamTests(DataStreamStreamingTests, PyFlinkStreamingTe
expected = ['(%s,%s)' % (index, total) for index in range(4) for total
in [1, 3]]
self.assert_equals_sorted(expected, self.test_sink.get_results())
+ def test_rows_in_containers(self):
+ self.addCleanup(self.env.set_parallelism, self.env.get_parallelism())
+ self.env.set_parallelism(1)
+ row_type = Types.ROW([Types.INT()])
+ output_type = Types.ROW([Types.OBJECT_ARRAY(row_type),
+ Types.LIST(row_type),
+ Types.MAP(Types.STRING(), row_type),
+
Types.OBJECT_ARRAY(Types.TUPLE([Types.INT()])),
+ Types.OBJECT_ARRAY(row_type)])
+
+ ds = self.env.from_collection([1], type_info=Types.INT()).map(
+ lambda _: Row([Row(1), Row(2)], [Row(3), Row(4)], {'a': Row(5),
'b': Row(6)},
+ [(7,), (8,)], None),
+ output_type=output_type)
+ # The second consumer keeps the map above from being fused with the
map below, so the
+ # rows are converted in both directions.
+ ds.add_sink(self.test_sink)
+ ds.map(lambda r: str(([v[0] for v in r[0]], [v[0] for v in r[1]],
+ sorted((k, v[0]) for k, v in r[2].items()),
+ [v[0] for v in r[3]], r[4])),
+ output_type=Types.STRING()) \
+ .add_sink(self.test_sink)
+ self.env.execute('test_rows_in_containers')
+ expected = ['+I[[+I[1], +I[2]], [+I[3], +I[4]], {a=+I[5], b=+I[6]},
[(7), (8)], null]',
+ "([1, 2], [3, 4], [('a', 5), ('b', 6)], [7, 8], None)"]
+ self.assert_equals_sorted(expected, self.test_sink.get_results(False))
+
def test_metrics(self):
ds = self.env.from_collection(
[('ab', 'a', decimal.Decimal(1)),
diff --git a/flink-python/pyflink/fn_execution/embedded/converters.py
b/flink-python/pyflink/fn_execution/embedded/converters.py
index 8829d48074e..b2b2f8d0c82 100644
--- a/flink-python/pyflink/fn_execution/embedded/converters.py
+++ b/flink-python/pyflink/fn_execution/embedded/converters.py
@@ -162,9 +162,15 @@ class ArrayDataConverter(ListDataConverter):
super(ArrayDataConverter, self).__init__(field_converter)
def to_internal(self, value) -> IN:
+ if value is None:
+ return None
+
return tuple(super(ArrayDataConverter, self).to_internal(value))
def to_external(self, value) -> OUT:
+ if value is None:
+ return None
+
return tuple(super(ArrayDataConverter, self).to_external(value))
@@ -234,7 +240,7 @@ def from_type_info_proto(type_info):
elif type_name in (type_info_name.BASIC_ARRAY,
type_info_name.OBJECT_ARRAY):
return
ArrayDataConverter(from_type_info_proto(type_info.collection_element_type))
- elif type_info == type_info_name.LIST:
+ elif type_name == type_info_name.LIST:
return
ListDataConverter(from_type_info_proto(type_info.collection_element_type))
elif type_name == type_info_name.MAP:
return
DictDataConverter(from_type_info_proto(type_info.map_type_info.key_type),
diff --git
a/flink-python/src/main/java/org/apache/flink/streaming/api/utils/PythonTypeUtils.java
b/flink-python/src/main/java/org/apache/flink/streaming/api/utils/PythonTypeUtils.java
index 7ee63382c1c..9b0414ccf12 100644
---
a/flink-python/src/main/java/org/apache/flink/streaming/api/utils/PythonTypeUtils.java
+++
b/flink-python/src/main/java/org/apache/flink/streaming/api/utils/PythonTypeUtils.java
@@ -988,16 +988,21 @@ public class PythonTypeUtils {
private static final long serialVersionUID = 1L;
private final DataConverter[] fieldDataConverters;
+ private final boolean reuse;
private final Row reuseRow;
private final Object[] reuseExternalRow;
private final Object[] reuseExternalRowData;
RowDataConverter(DataConverter[] dataConverters) {
+ this(dataConverters, true);
+ }
+
+ RowDataConverter(DataConverter[] dataConverters, boolean reuse) {
this.fieldDataConverters = dataConverters;
+ this.reuse = reuse;
this.reuseRow = new Row(fieldDataConverters.length);
this.reuseExternalRowData = new Object[fieldDataConverters.length];
this.reuseExternalRow = new Object[2];
- this.reuseExternalRow[1] = reuseExternalRowData;
}
@SuppressWarnings("unchecked")
@@ -1007,13 +1012,14 @@ public class PythonTypeUtils {
return null;
}
+ Row row = reuse ? reuseRow : new Row(fieldDataConverters.length);
RowKind rowKind = RowKind.fromByteValue(((Long)
value[0]).byteValue());
- reuseRow.setKind(rowKind);
+ row.setKind(rowKind);
Object[] fieldValues = (Object[]) value[1];
for (int i = 0; i < fieldValues.length; i++) {
- reuseRow.setField(i,
fieldDataConverters[i].toInternal(fieldValues[i]));
+ row.setField(i,
fieldDataConverters[i].toInternal(fieldValues[i]));
}
- return reuseRow;
+ return row;
}
@SuppressWarnings("unchecked")
@@ -1023,11 +1029,15 @@ public class PythonTypeUtils {
return null;
}
- reuseExternalRow[0] = (long) value.getKind().toByteValue();
+ Object[] externalRow = reuse ? reuseExternalRow : new Object[2];
+ Object[] externalRowData =
+ reuse ? reuseExternalRowData : new
Object[fieldDataConverters.length];
+ externalRow[0] = (long) value.getKind().toByteValue();
+ externalRow[1] = externalRowData;
for (int i = 0; i < value.getArity(); i++) {
- reuseExternalRowData[i] =
fieldDataConverters[i].toExternal(value.getField(i));
+ externalRowData[i] =
fieldDataConverters[i].toExternal(value.getField(i));
}
- return reuseExternalRow;
+ return externalRow;
}
@Override
@@ -1041,7 +1051,8 @@ public class PythonTypeUtils {
}
final RowDataConverter other = (RowDataConverter) o;
- if (other.fieldDataConverters.length !=
this.fieldDataConverters.length) {
+ if (other.reuse != this.reuse
+ || other.fieldDataConverters.length !=
this.fieldDataConverters.length) {
return false;
}
@@ -1101,11 +1112,13 @@ public class PythonTypeUtils {
public static final class TupleDataConverter extends DataConverter<Tuple,
Object[]> {
private final DataConverter[] fieldDataConverters;
+ private final boolean reuse;
private final Tuple reuseTuple;
private final Object[] reuseExternalTuple;
- TupleDataConverter(DataConverter[] dataConverters) {
+ TupleDataConverter(DataConverter[] dataConverters, boolean reuse) {
this.fieldDataConverters = dataConverters;
+ this.reuse = reuse;
this.reuseTuple = Tuple.newInstance(dataConverters.length);
this.reuseExternalTuple = new Object[dataConverters.length];
}
@@ -1117,10 +1130,11 @@ public class PythonTypeUtils {
return null;
}
+ Tuple tuple = reuse ? reuseTuple :
Tuple.newInstance(fieldDataConverters.length);
for (int i = 0; i < value.length; i++) {
-
reuseTuple.setField(fieldDataConverters[i].toInternal(value[i]), i);
+ tuple.setField(fieldDataConverters[i].toInternal(value[i]), i);
}
- return reuseTuple;
+ return tuple;
}
@SuppressWarnings("unchecked")
@@ -1130,10 +1144,12 @@ public class PythonTypeUtils {
return null;
}
+ Object[] externalTuple =
+ reuse ? reuseExternalTuple : new
Object[fieldDataConverters.length];
for (int i = 0; i < value.getArity(); i++) {
- reuseExternalTuple[i] =
fieldDataConverters[i].toExternal(value.getField(i));
+ externalTuple[i] =
fieldDataConverters[i].toExternal(value.getField(i));
}
- return reuseExternalTuple;
+ return externalTuple;
}
@Override
@@ -1147,7 +1163,8 @@ public class PythonTypeUtils {
}
final TupleDataConverter other = (TupleDataConverter) o;
- if (other.fieldDataConverters.length !=
this.fieldDataConverters.length) {
+ if (other.reuse != this.reuse
+ || other.fieldDataConverters.length !=
this.fieldDataConverters.length) {
return false;
}
@@ -1358,9 +1375,19 @@ public class PythonTypeUtils {
BasicTypeInfo.BYTE_TYPE_INFO.getTypeClass(),
ByteDataConverter.INSTANCE);
}
- @SuppressWarnings("unchecked")
public static <IN, OUT> DataConverter<IN, OUT> typeInfoDataConverter(
TypeInformation<IN> typeInformation) {
+ return typeInfoDataConverter(typeInformation, true);
+ }
+
+ /**
+ * Row and Tuple converters reuse one object per converter only when
{@code reuse} is true.
+ * Converters below an array, a list or a map don't reuse, since the
container keeps every
+ * converted element.
+ */
+ @SuppressWarnings("unchecked")
+ private static <IN, OUT> DataConverter<IN, OUT> typeInfoDataConverter(
+ TypeInformation<IN> typeInformation, boolean reuse) {
DataConverter<IN, OUT> dataConverter =
typeInfoToDataConverterMap.get(typeInformation.getTypeClass());
@@ -1374,27 +1401,29 @@ public class PythonTypeUtils {
if (typeInformation instanceof RowTypeInfo) {
DataConverter[] fieldDataConverters =
Arrays.stream(((RowTypeInfo)
typeInformation).getFieldTypes())
-
.map(TypeInfoToDataConverter::typeInfoDataConverter)
+ .map(fieldType ->
typeInfoDataConverter(fieldType, reuse))
.toArray(DataConverter[]::new);
- return (DataConverter<IN, OUT>) new
RowDataConverter(fieldDataConverters);
+ return (DataConverter<IN, OUT>)
+ new RowDataConverter(fieldDataConverters, reuse);
}
if (typeInformation instanceof TupleTypeInfo) {
- Object[] fieldDataConverters =
+ DataConverter[] fieldDataConverters =
Arrays.stream(((TupleTypeInfo)
typeInformation).getFieldTypes())
-
.map(TypeInfoToDataConverter::typeInfoDataConverter)
+ .map(fieldType ->
typeInfoDataConverter(fieldType, reuse))
.toArray(DataConverter[]::new);
return (DataConverter<IN, OUT>)
- new TupleDataConverter((DataConverter[])
fieldDataConverters);
+ new TupleDataConverter(fieldDataConverters, reuse);
}
if (typeInformation instanceof BasicArrayTypeInfo) {
return (DataConverter<IN, OUT>)
new ArrayDataConverter(
((BasicArrayTypeInfo)
typeInformation).getComponentTypeClass(),
-
TypeInfoToDataConverter.typeInfoDataConverter(
+ typeInfoDataConverter(
((BasicArrayTypeInfo)
typeInformation)
- .getComponentInfo()));
+ .getComponentInfo(),
+ false));
}
if (typeInformation instanceof ObjectArrayTypeInfo) {
@@ -1403,25 +1432,29 @@ public class PythonTypeUtils {
((ObjectArrayTypeInfo) typeInformation)
.getComponentInfo()
.getTypeClass(),
-
TypeInfoToDataConverter.typeInfoDataConverter(
+ typeInfoDataConverter(
((ObjectArrayTypeInfo)
typeInformation)
- .getComponentInfo()));
+ .getComponentInfo(),
+ false));
}
if (typeInformation instanceof ListTypeInfo) {
return (DataConverter<IN, OUT>)
new ListDataConverter(
-
TypeInfoToDataConverter.typeInfoDataConverter(
- ((ListTypeInfo)
typeInformation).getElementTypeInfo()));
+ typeInfoDataConverter(
+ ((ListTypeInfo)
typeInformation).getElementTypeInfo(),
+ false));
}
if (typeInformation instanceof MapTypeInfo) {
return (DataConverter<IN, OUT>)
new MapDataConverter(
-
TypeInfoToDataConverter.typeInfoDataConverter(
- ((MapTypeInfo)
typeInformation).getKeyTypeInfo()),
-
TypeInfoToDataConverter.typeInfoDataConverter(
- ((MapTypeInfo)
typeInformation).getValueTypeInfo()));
+ typeInfoDataConverter(
+ ((MapTypeInfo)
typeInformation).getKeyTypeInfo(),
+ false),
+ typeInfoDataConverter(
+ ((MapTypeInfo)
typeInformation).getValueTypeInfo(),
+ false));
}
if (typeInformation instanceof InternalTypeInfo) {
@@ -1433,9 +1466,10 @@ public class PythonTypeUtils {
if (typeInformation instanceof ExternalTypeInfo) {
return (DataConverter<IN, OUT>)
- TypeInfoToDataConverter.typeInfoDataConverter(
+ typeInfoDataConverter(
LegacyTypeInfoDataTypeConverter.toLegacyTypeInfo(
- ((ExternalTypeInfo<?>)
typeInformation).getDataType()));
+ ((ExternalTypeInfo<?>)
typeInformation).getDataType()),
+ reuse);
}
}
diff --git
a/flink-python/src/test/java/org/apache/flink/streaming/api/utils/PythonTypeUtilsTest.java
b/flink-python/src/test/java/org/apache/flink/streaming/api/utils/PythonTypeUtilsTest.java
index a45e4b6e3bf..12b0fb7ab99 100644
---
a/flink-python/src/test/java/org/apache/flink/streaming/api/utils/PythonTypeUtilsTest.java
+++
b/flink-python/src/test/java/org/apache/flink/streaming/api/utils/PythonTypeUtilsTest.java
@@ -36,6 +36,7 @@ import
org.apache.flink.api.common.typeutils.base.LongSerializer;
import org.apache.flink.api.common.typeutils.base.ShortSerializer;
import
org.apache.flink.api.common.typeutils.base.array.BytePrimitiveArraySerializer;
import
org.apache.flink.api.common.typeutils.base.array.IntPrimitiveArraySerializer;
+import org.apache.flink.api.java.tuple.Tuple1;
import org.apache.flink.api.java.typeutils.TupleTypeInfo;
import org.apache.flink.api.java.typeutils.runtime.RowSerializer;
import org.apache.flink.api.java.typeutils.runtime.TupleSerializer;
@@ -55,7 +56,9 @@ import org.apache.flink.types.Row;
import org.junit.jupiter.api.Test;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -292,4 +295,60 @@ class PythonTypeUtilsTest {
assertThat(dataConverter).isEqualTo(expectedDataConverter);
}
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void testDataConverterKeepsContainerElementsSeparate() {
+ Object[] external1 = new Object[] {0L, new Object[] {1L}};
+ Object[] external2 = new Object[] {0L, new Object[] {2L}};
+ Row row1 = Row.of(1);
+ Row row2 = Row.of(2);
+
+ PythonTypeUtils.DataConverter arrayConverter =
+ PythonTypeUtils.TypeInfoToDataConverter.typeInfoDataConverter(
+ Types.OBJECT_ARRAY(Types.ROW(Types.INT)));
+ assertThat((Object[]) arrayConverter.toInternal(new Object[]
{external1, external2}))
+ .containsExactly(row1, row2);
+ Object[] externalArray = (Object[]) arrayConverter.toExternal(new
Row[] {row1, row2});
+ assertThat((Object[]) ((Object[])
externalArray[0])[1]).containsExactly(1L);
+ assertThat((Object[]) ((Object[])
externalArray[1])[1]).containsExactly(2L);
+
+ PythonTypeUtils.DataConverter nestedArrayConverter =
+ PythonTypeUtils.TypeInfoToDataConverter.typeInfoDataConverter(
+ Types.OBJECT_ARRAY(Types.ROW(Types.ROW(Types.INT))));
+ assertThat(
+ (Object[])
+ nestedArrayConverter.toInternal(
+ new Object[] {
+ new Object[] {0L, new Object[]
{external1}},
+ new Object[] {0L, new Object[]
{external2}}
+ }))
+ .containsExactly(Row.of(row1), Row.of(row2));
+
+ PythonTypeUtils.DataConverter tupleArrayConverter =
+ PythonTypeUtils.TypeInfoToDataConverter.typeInfoDataConverter(
+ Types.OBJECT_ARRAY(Types.TUPLE(Types.INT)));
+ assertThat(
+ (Object[])
+ tupleArrayConverter.toInternal(
+ new Object[] {new Object[] {1L}, new
Object[] {2L}}))
+ .containsExactly(Tuple1.of(1), Tuple1.of(2));
+
+ PythonTypeUtils.DataConverter listConverter =
+ PythonTypeUtils.TypeInfoToDataConverter.typeInfoDataConverter(
+ Types.LIST(Types.ROW(Types.INT)));
+ assertThat((List<Object>)
listConverter.toInternal(Arrays.asList(external1, external2)))
+ .containsExactly(row1, row2);
+
+ PythonTypeUtils.DataConverter mapConverter =
+ PythonTypeUtils.TypeInfoToDataConverter.typeInfoDataConverter(
+ Types.MAP(Types.STRING, Types.ROW(Types.INT)));
+ Map<String, Object> externalMap = new HashMap<>();
+ externalMap.put("a", external1);
+ externalMap.put("b", external2);
+ Map<String, Row> expectedMap = new HashMap<>();
+ expectedMap.put("a", row1);
+ expectedMap.put("b", row2);
+ assertThat((Map<String, Row>)
mapConverter.toInternal(externalMap)).isEqualTo(expectedMap);
+ }
}