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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 3341f3b3cb [Fix][Connector-V2] Fix Paimon nested row write (#10888)
3341f3b3cb is described below

commit 3341f3b3cb079eee5b6dd6df7cd732e86d054d10
Author: QuakeWang <[email protected]>
AuthorDate: Wed May 27 22:09:45 2026 +0800

    [Fix][Connector-V2] Fix Paimon nested row write (#10888)
    
    Signed-off-by: QuakeWang <[email protected]>
---
 .../seatunnel/paimon/utils/RowConverter.java       |  31 ++--
 .../seatunnel/paimon/utils/RowTypeConverter.java   |  13 +-
 .../seatunnel/paimon/utils/RowConverterTest.java   | 186 ++++++++++++++++++++-
 .../paimon/utils/RowTypeConverterTest.java         |  16 ++
 4 files changed, 226 insertions(+), 20 deletions(-)

diff --git 
a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowConverter.java
 
b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowConverter.java
index c47e5e140b..e25de32f24 100644
--- 
a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowConverter.java
+++ 
b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowConverter.java
@@ -365,13 +365,17 @@ public class RowConverter {
             SeaTunnelRow seaTunnelRow,
             SeaTunnelRowType seaTunnelRowType,
             TableSchema sinkTableSchema) {
-        List<DataField> sinkTotalFields = sinkTableSchema.fields();
+        return reconvert(seaTunnelRow, seaTunnelRowType, 
sinkTableSchema.fields());
+    }
+
+    private static InternalRow reconvert(
+            SeaTunnelRow seaTunnelRow,
+            SeaTunnelRowType seaTunnelRowType,
+            List<DataField> sinkFields) {
         int sourceTotalFields = seaTunnelRowType.getTotalFields();
-        if (sourceTotalFields != sinkTotalFields.size()) {
+        if (sourceTotalFields != sinkFields.size()) {
             throw CommonError.writeRowErrorWithFieldsCountNotMatch(
-                    PaimonBaseOptions.CONNECTOR_IDENTITY,
-                    sourceTotalFields,
-                    sinkTotalFields.size());
+                    PaimonBaseOptions.CONNECTOR_IDENTITY, sourceTotalFields, 
sinkFields.size());
         }
         BinaryRow binaryRow = new BinaryRow(sourceTotalFields);
         BinaryWriter binaryWriter = new BinaryRowWriter(binaryRow);
@@ -393,7 +397,7 @@ public class RowConverter {
                 binaryWriter.setNullAt(i);
                 continue;
             }
-            checkCanWriteWithSchema(i, seaTunnelRowType, sinkTotalFields, 
fieldValue);
+            checkCanWriteWithSchema(i, seaTunnelRowType, sinkFields, 
fieldValue);
             String fieldName = seaTunnelRowType.getFieldName(i);
             switch (fieldTypes[i].getSqlType()) {
                 case TINYINT:
@@ -415,8 +419,7 @@ public class RowConverter {
                     binaryWriter.writeDouble(i, (Double) fieldValue);
                     break;
                 case DECIMAL:
-                    DataField decimalDataField =
-                            SchemaUtil.getDataField(sinkTotalFields, 
fieldName);
+                    DataField decimalDataField = 
SchemaUtil.getDataField(sinkFields, fieldName);
                     org.apache.paimon.types.DecimalType decimalType =
                             (org.apache.paimon.types.DecimalType) 
decimalDataField.type();
                     binaryWriter.writeDecimal(
@@ -442,7 +445,7 @@ public class RowConverter {
                             .setValue(binaryWriter, i, 
DateTimeUtils.toInternal(date));
                     break;
                 case TIMESTAMP:
-                    DataField dataField = 
SchemaUtil.getDataField(sinkTotalFields, fieldName);
+                    DataField dataField = SchemaUtil.getDataField(sinkFields, 
fieldName);
                     int precision = ((TimestampType) 
dataField.type()).getPrecision();
                     LocalDateTime datetime = (LocalDateTime) fieldValue;
                     binaryWriter.writeTimestamp(
@@ -483,11 +486,13 @@ public class RowConverter {
                 case ROW:
                     SeaTunnelDataType<?> rowType = 
seaTunnelRowType.getFieldType(i);
                     Object row = fieldValue;
-                    TableSchema nestedSchema = 
extractNestedSchema(sinkTotalFields.get(i).type());
-                    InternalRow paimonRow =
-                            reconvert((SeaTunnelRow) row, (SeaTunnelRowType) 
rowType, nestedSchema);
                     RowType paimonRowType =
-                            RowTypeConverter.reconvert((SeaTunnelRowType) 
rowType, nestedSchema);
+                            (RowType) SchemaUtil.getDataField(sinkFields, 
fieldName).type();
+                    InternalRow paimonRow =
+                            reconvert(
+                                    (SeaTunnelRow) row,
+                                    (SeaTunnelRowType) rowType,
+                                    paimonRowType.getFields());
                     binaryWriter.writeRow(i, paimonRow, new 
InternalRowSerializer(paimonRowType));
                     break;
                 default:
diff --git 
a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowTypeConverter.java
 
b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowTypeConverter.java
index ef9d24c3d4..df437f1ab5 100644
--- 
a/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowTypeConverter.java
+++ 
b/seatunnel-connectors-v2/connector-paimon/src/main/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowTypeConverter.java
@@ -422,13 +422,16 @@ public class RowTypeConverter {
                     SeaTunnelDataType<?>[] fieldTypes = row.getFieldTypes();
                     String[] fieldNames = row.getFieldNames();
                     int totalFields = row.getTotalFields();
-                    DataType[] dataTypes = new DataType[totalFields];
+                    DataField[] dataFields = new DataField[totalFields];
                     for (int i = 0; i < totalFields; i++) {
-                        dataTypes[i] =
-                                SeaTunnelTypeToPaimonVisitor.INSTANCE.visit(
-                                        fieldNames[i], fieldTypes[i]);
+                        dataFields[i] =
+                                new DataField(
+                                        i,
+                                        fieldNames[i],
+                                        
SeaTunnelTypeToPaimonVisitor.INSTANCE.visit(
+                                                fieldNames[i], fieldTypes[i]));
                     }
-                    return DataTypes.ROW(dataTypes);
+                    return DataTypes.ROW(dataFields);
                 default:
                     throw CommonError.unsupportedDataType(
                             PaimonBaseOptions.CONNECTOR_IDENTITY,
diff --git 
a/seatunnel-connectors-v2/connector-paimon/src/test/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowConverterTest.java
 
b/seatunnel-connectors-v2/connector-paimon/src/test/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowConverterTest.java
index f62884c005..92d26e523f 100644
--- 
a/seatunnel-connectors-v2/connector-paimon/src/test/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowConverterTest.java
+++ 
b/seatunnel-connectors-v2/connector-paimon/src/test/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowConverterTest.java
@@ -369,6 +369,152 @@ public class RowConverterTest {
                 });
     }
 
+    @Test
+    public void seaTunnelToPaimonWithNestedRow() {
+        SeaTunnelRowType sourceType = nestedRowType(BasicType.STRING_TYPE);
+        SeaTunnelRow data = nestedRowData("Alice");
+        RowType sinkRowType =
+                nestedPaimonRowType(
+                        RowType.of(
+                                new DataType[] {DataTypes.BIGINT(), 
DataTypes.STRING()},
+                                new String[] {"id", "name"}));
+
+        InternalRow actual = RowConverter.reconvert(data, sourceType, 
tableSchema(sinkRowType));
+
+        Assertions.assertEquals(1L, actual.getLong(0));
+        InternalRow buyer = actual.getRow(1, 2);
+        Assertions.assertEquals(1001L, buyer.getLong(0));
+        Assertions.assertEquals("Alice", buyer.getString(1).toString());
+        Assertions.assertEquals(
+                new BigDecimal("20.50"), actual.getDecimal(2, 10, 
2).toBigDecimal());
+    }
+
+    @Test
+    public void seaTunnelToPaimonWithNestedRowFieldCountMismatch() {
+        SeaTunnelRowType sourceType = nestedRowType(BasicType.STRING_TYPE);
+        SeaTunnelRow data = nestedRowData("Alice");
+        RowType sinkRowType =
+                nestedPaimonRowType(
+                        RowType.of(new DataType[] {DataTypes.BIGINT()}, new 
String[] {"id"}));
+
+        SeaTunnelRuntimeException actual =
+                Assertions.assertThrows(
+                        SeaTunnelRuntimeException.class,
+                        () -> RowConverter.reconvert(data, sourceType, 
tableSchema(sinkRowType)));
+
+        Assertions.assertEquals(
+                CommonError.writeRowErrorWithFieldsCountNotMatch("Paimon", 2, 
1).getMessage(),
+                actual.getMessage());
+    }
+
+    @Test
+    public void seaTunnelToPaimonRejectsIncompatibleNestedRowSchema() {
+        SeaTunnelRowType sourceType = nestedRowType(BasicType.STRING_TYPE);
+        SeaTunnelRow data = nestedRowData("Alice");
+        RowType sinkBuyerRowType =
+                RowType.of(
+                        new DataType[] {DataTypes.BIGINT(), DataTypes.INT()},
+                        new String[] {"id", "name"});
+        RowType sinkRowType = nestedPaimonRowType(sinkBuyerRowType);
+
+        SeaTunnelRuntimeException actual =
+                Assertions.assertThrows(
+                        SeaTunnelRuntimeException.class,
+                        () -> RowConverter.reconvert(data, sourceType, 
tableSchema(sinkRowType)));
+
+        Assertions.assertEquals(
+                CommonError.writeRowErrorWithSchemaIncompatibleSchema(
+                                "Paimon",
+                                "name" + StringUtils.SPACE + "STRING",
+                                new DataField(1, "name", 
DataTypes.STRING()).asSQLString(),
+                                
sinkBuyerRowType.getFields().get(1).asSQLString())
+                        .getMessage(),
+                actual.getMessage());
+    }
+
+    @Test
+    public void seaTunnelToPaimonRejectsNestedRowFieldNameMismatch() {
+        SeaTunnelRowType sourceType = nestedRowType(BasicType.STRING_TYPE);
+        SeaTunnelRow data = nestedRowData("Alice");
+        RowType sinkBuyerRowType =
+                RowType.of(
+                        new DataType[] {DataTypes.BIGINT(), 
DataTypes.STRING()},
+                        new String[] {"id", "full_name"});
+        RowType sinkRowType = nestedPaimonRowType(sinkBuyerRowType);
+
+        SeaTunnelRuntimeException actual =
+                Assertions.assertThrows(
+                        SeaTunnelRuntimeException.class,
+                        () -> RowConverter.reconvert(data, sourceType, 
tableSchema(sinkRowType)));
+
+        Assertions.assertEquals(
+                CommonError.writeRowErrorWithSchemaIncompatibleSchema(
+                                "Paimon",
+                                "name" + StringUtils.SPACE + "STRING",
+                                new DataField(1, "name", 
DataTypes.STRING()).asSQLString(),
+                                
sinkBuyerRowType.getFields().get(1).asSQLString())
+                        .getMessage(),
+                actual.getMessage());
+    }
+
+    @Test
+    public void seaTunnelToPaimonWithNullNestedRow() {
+        SeaTunnelRowType sourceType = nestedRowType(BasicType.STRING_TYPE);
+        SeaTunnelRow data = new SeaTunnelRow(new Object[] {1L, null, new 
BigDecimal("20.50")});
+        RowType sinkRowType =
+                nestedPaimonRowType(
+                        RowType.of(
+                                new DataType[] {DataTypes.BIGINT(), 
DataTypes.STRING()},
+                                new String[] {"id", "name"}));
+
+        InternalRow actual = RowConverter.reconvert(data, sourceType, 
tableSchema(sinkRowType));
+
+        Assertions.assertTrue(actual.isNullAt(1));
+    }
+
+    @Test
+    public void seaTunnelToPaimonWithNestedRowPrecision() {
+        SeaTunnelRowType buyerType =
+                new SeaTunnelRowType(
+                        new String[] {"created_at", "score"},
+                        new SeaTunnelDataType<?>[] {
+                            LocalTimeType.LOCAL_DATE_TIME_TYPE, new 
DecimalType(6, 2)
+                        });
+        SeaTunnelRowType sourceType =
+                new SeaTunnelRowType(
+                        new String[] {"order_id", "buyer", "amount"},
+                        new SeaTunnelDataType<?>[] {
+                            BasicType.LONG_TYPE, buyerType, new 
DecimalType(10, 2)
+                        });
+        LocalDateTime createdAt = LocalDateTime.of(2024, 1, 2, 3, 4, 5, 
123000000);
+        SeaTunnelRow data =
+                new SeaTunnelRow(
+                        new Object[] {
+                            1L,
+                            new SeaTunnelRow(new Object[] {createdAt, new 
BigDecimal("12.34")}),
+                            new BigDecimal("20.50")
+                        });
+        RowType sinkRowType =
+                RowType.of(
+                        new DataType[] {
+                            DataTypes.BIGINT(),
+                            RowType.of(
+                                    new DataType[] {
+                                        DataTypes.TIMESTAMP(3), 
DataTypes.DECIMAL(8, 4)
+                                    },
+                                    new String[] {"created_at", "score"}),
+                            DataTypes.DECIMAL(10, 2)
+                        },
+                        new String[] {"order_id", "buyer", "amount"});
+
+        InternalRow actual = RowConverter.reconvert(data, sourceType, 
tableSchema(sinkRowType));
+
+        InternalRow buyer = actual.getRow(1, 2);
+        Assertions.assertEquals(createdAt, buyer.getTimestamp(0, 
3).toLocalDateTime());
+        Assertions.assertEquals(
+                0, new BigDecimal("12.34").compareTo(buyer.getDecimal(1, 8, 
4).toBigDecimal()));
+    }
+
     @Test
     public void nestedRowPaimonToSeaTunnel() {
         // Top-level: c_int INT, c_string STRING, c_nested ROW<a INT, b STRING>
@@ -417,7 +563,7 @@ public class RowConverterTest {
                             BasicType.INT_TYPE, BasicType.STRING_TYPE, 
seaTunnelNestedType
                         });
 
-        // Convert Paimon → SeaTunnel
+        // Convert Paimon to SeaTunnel
         SeaTunnelRow result = RowConverter.convert(binaryRow, 
seaTunnelTopLevelType, tableSchema);
 
         Assertions.assertEquals(1, result.getField(0));
@@ -465,7 +611,7 @@ public class RowConverterTest {
         SeaTunnelRow nestedRow = new SeaTunnelRow(new Object[] {42, "world"});
         SeaTunnelRow topLevelRow = new SeaTunnelRow(new Object[] {1, "hello", 
nestedRow});
 
-        // Convert SeaTunnel → Paimon (would throw before fix due to field 
count mismatch)
+        // Convert SeaTunnel to Paimon (would throw before fix due to field 
count mismatch)
         InternalRow result =
                 RowConverter.reconvert(topLevelRow, seaTunnelTopLevelType, 
tableSchema);
 
@@ -475,4 +621,40 @@ public class RowConverterTest {
         Assertions.assertEquals(42, nestedResult.getInt(0));
         Assertions.assertEquals("world", nestedResult.getString(1).toString());
     }
+
+    private static SeaTunnelRowType nestedRowType(SeaTunnelDataType<?> 
buyerNameType) {
+        SeaTunnelRowType buyerType =
+                new SeaTunnelRowType(
+                        new String[] {"id", "name"},
+                        new SeaTunnelDataType<?>[] {BasicType.LONG_TYPE, 
buyerNameType});
+        return new SeaTunnelRowType(
+                new String[] {"order_id", "buyer", "amount"},
+                new SeaTunnelDataType<?>[] {
+                    BasicType.LONG_TYPE, buyerType, new DecimalType(10, 2)
+                });
+    }
+
+    private static SeaTunnelRow nestedRowData(String buyerName) {
+        return new SeaTunnelRow(
+                new Object[] {
+                    1L, new SeaTunnelRow(new Object[] {1001L, buyerName}), new 
BigDecimal("20.50")
+                });
+    }
+
+    private static RowType nestedPaimonRowType(RowType buyerType) {
+        return RowType.of(
+                new DataType[] {DataTypes.BIGINT(), buyerType, 
DataTypes.DECIMAL(10, 2)},
+                new String[] {"order_id", "buyer", "amount"});
+    }
+
+    private static TableSchema tableSchema(RowType rowType) {
+        return new TableSchema(
+                0,
+                TableSchema.newFields(rowType),
+                rowType.getFieldCount(),
+                Collections.emptyList(),
+                Collections.emptyList(),
+                Collections.emptyMap(),
+                "");
+    }
 }
diff --git 
a/seatunnel-connectors-v2/connector-paimon/src/test/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowTypeConverterTest.java
 
b/seatunnel-connectors-v2/connector-paimon/src/test/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowTypeConverterTest.java
index 0d29320768..1ae7fa1396 100644
--- 
a/seatunnel-connectors-v2/connector-paimon/src/test/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowTypeConverterTest.java
+++ 
b/seatunnel-connectors-v2/connector-paimon/src/test/java/org/apache/seatunnel/connectors/seatunnel/paimon/utils/RowTypeConverterTest.java
@@ -231,6 +231,22 @@ public class RowTypeConverterTest {
         Assertions.assertEquals(convert, rowType);
     }
 
+    @Test
+    public void seaTunnelNestedRowToPaimonKeepsFieldNames() {
+        SeaTunnelRowType buyerType =
+                new SeaTunnelRowType(
+                        new String[] {"id", "name"},
+                        new SeaTunnelDataType<?>[] {BasicType.LONG_TYPE, 
BasicType.STRING_TYPE});
+
+        DataType convert = RowTypeConverter.reconvert("buyer", buyerType);
+
+        RowType expected =
+                RowType.of(
+                        new DataType[] {DataTypes.BIGINT(), 
DataTypes.STRING()},
+                        new String[] {"id", "name"});
+        Assertions.assertEquals(expected, convert);
+    }
+
     @Test
     public void paimonDataTypeToSeaTunnelColumn() {
         Column column = RowTypeConverter.convert(typeDefine);

Reply via email to