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

luoyuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new 8cd8d1628 [lake] Fix union read duplicating rows for non-String 
partition columns (#3500)
8cd8d1628 is described below

commit 8cd8d16283d83a6b9dabb1e41d0cf2e41219f34a
Author: bryndenZh <[email protected]>
AuthorDate: Sat Jul 4 14:44:24 2026 +0800

    [lake] Fix union read duplicating rows for non-String partition columns 
(#3500)
    
    ---------
    
    Co-authored-by: luoyuxia <[email protected]>
---
 .../fluss/lake/paimon/source/PaimonSplit.java      |  23 +--
 .../lake/paimon/source/PaimonSplitPlanner.java     |  13 +-
 .../lake/paimon/source/PaimonSplitSerializer.java  |  42 ++++-
 .../utils/DvTableReadableSnapshotRetriever.java    |  15 +-
 .../fluss/lake/paimon/utils/PaimonConversions.java |  27 ++++
 .../utils/PaimonDataTypeToFlussDataType.java       | 175 +++++++++++++++++++++
 .../paimon/source/PaimonSplitSerializerTest.java   |  97 ++++++------
 .../fluss/lake/paimon/source/PaimonSplitTest.java  |  30 ++++
 .../lake/paimon/utils/PaimonConversionsTest.java   | 145 +++++++++++++++++
 9 files changed, 489 insertions(+), 78 deletions(-)

diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplit.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplit.java
index 925377067..ed47361c3 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplit.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplit.java
@@ -20,11 +20,8 @@ package org.apache.fluss.lake.paimon.source;
 
 import org.apache.fluss.lake.source.LakeSplit;
 
-import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.table.source.DataSplit;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 
 /** Split for paimon table. */
@@ -36,9 +33,13 @@ public class PaimonSplit implements LakeSplit {
 
     private final boolean isBucketUnAware;
 
-    public PaimonSplit(DataSplit dataSplit, boolean isBucketUnAware) {
+    // Partition values in Fluss partition-name format
+    private final List<String> partition;
+
+    public PaimonSplit(DataSplit dataSplit, boolean isBucketUnAware, 
List<String> partition) {
         this.dataSplit = dataSplit;
         this.isBucketUnAware = isBucketUnAware;
+        this.partition = partition;
     }
 
     @Override
@@ -52,19 +53,7 @@ public class PaimonSplit implements LakeSplit {
 
     @Override
     public List<String> partition() {
-        BinaryRow partition = dataSplit.partition();
-        if (partition.getFieldCount() == 0) {
-            return Collections.emptyList();
-        }
-
-        List<String> partitions = new ArrayList<>();
-        for (int i = 0; i < partition.getFieldCount(); i++) {
-            // Todo Currently, partition column must be String datatype, so we 
can always use
-            // consider it as string. Revisit here when
-            // #489 is finished.
-            partitions.add(partition.getString(i).toString());
-        }
-        return partitions;
+        return partition;
     }
 
     public DataSplit dataSplit() {
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplitPlanner.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplitPlanner.java
index db723f581..e20e3fe8b 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplitPlanner.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplitPlanner.java
@@ -21,11 +21,13 @@ package org.apache.fluss.lake.paimon.source;
 import org.apache.fluss.config.Configuration;
 import org.apache.fluss.lake.source.Planner;
 import org.apache.fluss.metadata.TablePath;
+import org.apache.fluss.types.RowType;
 
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.catalog.Catalog;
 import org.apache.paimon.catalog.CatalogContext;
 import org.apache.paimon.catalog.CatalogFactory;
+import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.table.BucketMode;
@@ -40,6 +42,8 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import static 
org.apache.fluss.lake.paimon.utils.PaimonConversions.toFlussPartitionValues;
+import static 
org.apache.fluss.lake.paimon.utils.PaimonConversions.toFlussRowType;
 import static org.apache.fluss.lake.paimon.utils.PaimonConversions.toPaimon;
 
 /** Split panner for paimon table. */
@@ -69,13 +73,20 @@ public class PaimonSplitPlanner implements 
Planner<PaimonSplit> {
                 FileStoreTable fileStoreTable = getTable(catalog, tablePath, 
snapshotId);
                 InnerTableScan tableScan = fileStoreTable.newScan();
                 boolean isBucketUnAware = fileStoreTable.bucketMode() == 
BucketMode.BUCKET_UNAWARE;
+                RowType flussPartitionType =
+                        
toFlussRowType(fileStoreTable.schema().logicalPartitionType());
 
                 if (predicate != null) {
                     tableScan = tableScan.withFilter(predicate);
                 }
                 for (Split split : tableScan.plan().splits()) {
                     DataSplit dataSplit = (DataSplit) split;
-                    splits.add(new PaimonSplit(dataSplit, isBucketUnAware));
+                    BinaryRow partitionRow = dataSplit.partition();
+                    List<String> partition =
+                            partitionRow.getFieldCount() == 0
+                                    ? Collections.emptyList()
+                                    : toFlussPartitionValues(partitionRow, 
flussPartitionType);
+                    splits.add(new PaimonSplit(dataSplit, isBucketUnAware, 
partition));
                 }
             }
             return splits;
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplitSerializer.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplitSerializer.java
index c5e4d38d1..147a79ef1 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplitSerializer.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/source/PaimonSplitSerializer.java
@@ -20,6 +20,7 @@ package org.apache.fluss.lake.paimon.source;
 
 import org.apache.fluss.lake.serializer.SimpleVersionedSerializer;
 
+import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.io.DataOutputViewStreamWrapper;
 import org.apache.paimon.table.source.DataSplit;
 import org.apache.paimon.utils.InstantiationUtil;
@@ -28,15 +29,20 @@ import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 
 /** Serializer for paimon split. */
 public class PaimonSplitSerializer implements 
SimpleVersionedSerializer<PaimonSplit> {
 
     private static final int VERSION_1 = 1;
+    // VERSION_2 additionally persists the partition values.
+    private static final int VERSION_2 = 2;
 
     @Override
     public int getVersion() {
-        return VERSION_1;
+        return VERSION_2;
     }
 
     @Override
@@ -46,6 +52,11 @@ public class PaimonSplitSerializer implements 
SimpleVersionedSerializer<PaimonSp
         DataSplit dataSplit = paimonSplit.dataSplit();
         InstantiationUtil.serializeObject(view, dataSplit);
         view.writeBoolean(paimonSplit.isBucketUnAware());
+        List<String> partition = paimonSplit.partition();
+        view.writeInt(partition.size());
+        for (String value : partition) {
+            view.writeUTF(value);
+        }
         return out.toByteArray();
     }
 
@@ -55,11 +66,20 @@ public class PaimonSplitSerializer implements 
SimpleVersionedSerializer<PaimonSp
         DataSplit dataSplit;
         try {
             dataSplit = InstantiationUtil.deserializeObject(in, 
getClass().getClassLoader());
-
+            DataInputStream dis = new DataInputStream(in);
+            boolean isBucketUnAware = dis.readBoolean();
             if (version == VERSION_1) {
-                DataInputStream dis = new DataInputStream(in);
-                boolean isBucketUnAware = dis.readBoolean();
-                return new PaimonSplit(dataSplit, isBucketUnAware);
+                // VERSION_1 did not store partition values separately, but 
string partitions were
+                // exposed through DataSplit.partition(). Preserve that old 
behavior.
+                return new PaimonSplit(
+                        dataSplit, isBucketUnAware, 
readStringPartition(dataSplit.partition()));
+            } else if (version == VERSION_2) {
+                int size = dis.readInt();
+                List<String> partition = new ArrayList<>(size);
+                for (int i = 0; i < size; i++) {
+                    partition.add(dis.readUTF());
+                }
+                return new PaimonSplit(dataSplit, isBucketUnAware, partition);
             } else {
                 throw new IOException("Unsupported PaimonSplit serialization 
version: " + version);
             }
@@ -67,4 +87,16 @@ public class PaimonSplitSerializer implements 
SimpleVersionedSerializer<PaimonSp
             throw new IOException("Failed to deserialize PaimonSplit", e);
         }
     }
+
+    private List<String> readStringPartition(BinaryRow partition) {
+        if (partition == null || partition.getFieldCount() == 0) {
+            return Collections.emptyList();
+        }
+
+        List<String> partitions = new ArrayList<>(partition.getFieldCount());
+        for (int i = 0; i < partition.getFieldCount(); i++) {
+            partitions.add(partition.getString(i).toString());
+        }
+        return partitions;
+    }
 }
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/DvTableReadableSnapshotRetriever.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/DvTableReadableSnapshotRetriever.java
index 54c3c7f94..0d55edc48 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/DvTableReadableSnapshotRetriever.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/DvTableReadableSnapshotRetriever.java
@@ -35,7 +35,6 @@ import org.apache.fluss.utils.types.Tuple2;
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.Snapshot;
 import org.apache.paimon.data.BinaryRow;
-import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.manifest.FileKind;
 import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.operation.FileStoreScan;
@@ -48,7 +47,6 @@ import org.slf4j.LoggerFactory;
 import javax.annotation.Nullable;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -601,13 +599,12 @@ public class DvTableReadableSnapshotRetriever implements 
AutoCloseable {
      * @return partition name string
      */
     private String getPartitionNameFromBinaryRow(BinaryRow partition) {
-        List<String> partitionValues = new ArrayList<>();
-        for (int i = 0; i < partition.getFieldCount(); i++) {
-            // todo: consider other partition type
-            BinaryString binaryString = partition.getString(i);
-            partitionValues.add(binaryString.toString());
-        }
-        return String.join(ResolvedPartitionSpec.PARTITION_SPEC_SEPARATOR, 
partitionValues);
+        return String.join(
+                ResolvedPartitionSpec.PARTITION_SPEC_SEPARATOR,
+                PaimonConversions.toFlussPartitionValues(
+                        partition,
+                        PaimonConversions.toFlussRowType(
+                                
fileStoreTable.schema().logicalPartitionType())));
     }
 
     @Override
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/PaimonConversions.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/PaimonConversions.java
index 94166e2d0..4bb12a0f4 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/PaimonConversions.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/PaimonConversions.java
@@ -28,12 +28,15 @@ import org.apache.fluss.record.ChangeType;
 import org.apache.fluss.row.GenericRow;
 import org.apache.fluss.row.InternalRow;
 import org.apache.fluss.types.DataTypeRoot;
+import org.apache.fluss.utils.PartitionUtils;
 
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.SchemaChange;
+import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.RowKind;
 import org.apache.paimon.types.RowType;
@@ -116,6 +119,30 @@ public class PaimonConversions {
                 .getFieldOrNull(flussRowAsPaimonRow);
     }
 
+    public static org.apache.fluss.types.RowType toFlussRowType(RowType 
paimonRowType) {
+        org.apache.fluss.types.RowType.Builder builder = 
org.apache.fluss.types.RowType.builder();
+        for (DataField field : paimonRowType.getFields()) {
+            builder.field(
+                    field.name(), 
field.type().accept(PaimonDataTypeToFlussDataType.INSTANCE));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Renders a Paimon partition row into Fluss partition value strings, in 
partition-key order.
+     */
+    public static List<String> toFlussPartitionValues(
+            BinaryRow partition, org.apache.fluss.types.RowType 
flussPartitionType) {
+        PaimonRowAsFlussRow flussRow = new 
PaimonRowAsFlussRow().replaceRow(partition);
+        List<String> values = new ArrayList<>(partition.getFieldCount());
+        for (int i = 0; i < partition.getFieldCount(); i++) {
+            org.apache.fluss.types.DataType flussType = 
flussPartitionType.getTypeAt(i);
+            Object value = InternalRow.createFieldGetter(flussType, 
i).getFieldOrNull(flussRow);
+            values.add(PartitionUtils.convertValueOfType(value, 
flussType.getTypeRoot()));
+        }
+        return values;
+    }
+
     public static List<SchemaChange> toPaimonSchemaChanges(List<TableChange> 
tableChanges) {
         List<SchemaChange> schemaChanges = new 
ArrayList<>(tableChanges.size());
 
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/PaimonDataTypeToFlussDataType.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/PaimonDataTypeToFlussDataType.java
new file mode 100644
index 000000000..36f4678f1
--- /dev/null
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/utils/PaimonDataTypeToFlussDataType.java
@@ -0,0 +1,175 @@
+/*
+ * 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.fluss.lake.paimon.utils;
+
+import org.apache.fluss.types.DataTypes;
+
+import org.apache.paimon.types.ArrayType;
+import org.apache.paimon.types.BigIntType;
+import org.apache.paimon.types.BinaryType;
+import org.apache.paimon.types.BooleanType;
+import org.apache.paimon.types.CharType;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypeDefaultVisitor;
+import org.apache.paimon.types.DateType;
+import org.apache.paimon.types.DecimalType;
+import org.apache.paimon.types.DoubleType;
+import org.apache.paimon.types.FloatType;
+import org.apache.paimon.types.IntType;
+import org.apache.paimon.types.LocalZonedTimestampType;
+import org.apache.paimon.types.MapType;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.SmallIntType;
+import org.apache.paimon.types.TimeType;
+import org.apache.paimon.types.TimestampType;
+import org.apache.paimon.types.TinyIntType;
+import org.apache.paimon.types.VarBinaryType;
+import org.apache.paimon.types.VarCharType;
+
+/**
+ * Convert from Paimon's data type to Fluss's data type (inverse of {@link
+ * FlussDataTypeToPaimonDataType}).
+ */
+public class PaimonDataTypeToFlussDataType
+        extends DataTypeDefaultVisitor<org.apache.fluss.types.DataType> {
+
+    public static final PaimonDataTypeToFlussDataType INSTANCE =
+            new PaimonDataTypeToFlussDataType();
+
+    @Override
+    public org.apache.fluss.types.DataType visit(CharType charType) {
+        return withNullability(DataTypes.CHAR(charType.getLength()), charType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(VarCharType varCharType) {
+        return withNullability(DataTypes.STRING(), varCharType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(BooleanType booleanType) {
+        return withNullability(DataTypes.BOOLEAN(), booleanType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(BinaryType binaryType) {
+        return withNullability(DataTypes.BINARY(binaryType.getLength()), 
binaryType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(VarBinaryType varBinaryType) {
+        return withNullability(DataTypes.BYTES(), varBinaryType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(DecimalType decimalType) {
+        return withNullability(
+                DataTypes.DECIMAL(decimalType.getPrecision(), 
decimalType.getScale()), decimalType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(TinyIntType tinyIntType) {
+        return withNullability(DataTypes.TINYINT(), tinyIntType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(SmallIntType smallIntType) {
+        return withNullability(DataTypes.SMALLINT(), smallIntType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(IntType intType) {
+        return withNullability(DataTypes.INT(), intType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(BigIntType bigIntType) {
+        return withNullability(DataTypes.BIGINT(), bigIntType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(FloatType floatType) {
+        return withNullability(DataTypes.FLOAT(), floatType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(DoubleType doubleType) {
+        return withNullability(DataTypes.DOUBLE(), doubleType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(DateType dateType) {
+        return withNullability(DataTypes.DATE(), dateType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(TimeType timeType) {
+        return withNullability(DataTypes.TIME(timeType.getPrecision()), 
timeType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(TimestampType timestampType) {
+        return 
withNullability(DataTypes.TIMESTAMP(timestampType.getPrecision()), 
timestampType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(LocalZonedTimestampType 
localZonedTimestampType) {
+        return withNullability(
+                
DataTypes.TIMESTAMP_LTZ(localZonedTimestampType.getPrecision()),
+                localZonedTimestampType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(ArrayType arrayType) {
+        return 
withNullability(DataTypes.ARRAY(arrayType.getElementType().accept(this)), 
arrayType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(MapType mapType) {
+        return withNullability(
+                DataTypes.MAP(
+                        mapType.getKeyType().accept(this), 
mapType.getValueType().accept(this)),
+                mapType);
+    }
+
+    @Override
+    public org.apache.fluss.types.DataType visit(RowType rowType) {
+        org.apache.fluss.types.RowType.Builder builder = 
org.apache.fluss.types.RowType.builder();
+        for (DataField field : rowType.getFields()) {
+            org.apache.fluss.types.DataType fieldType = 
field.type().accept(this);
+            if (field.description() == null) {
+                builder.field(field.name(), fieldType);
+            } else {
+                builder.field(field.name(), fieldType, field.description());
+            }
+        }
+        return withNullability(builder.build(), rowType);
+    }
+
+    @Override
+    protected org.apache.fluss.types.DataType defaultMethod(DataType dataType) 
{
+        throw new UnsupportedOperationException(
+                "Unsupported data type to convert to Fluss: " + 
dataType.getTypeRoot());
+    }
+
+    private static org.apache.fluss.types.DataType withNullability(
+            org.apache.fluss.types.DataType flussType, DataType paimonType) {
+        return flussType.copy(paimonType.isNullable());
+    }
+}
diff --git 
a/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/source/PaimonSplitSerializerTest.java
 
b/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/source/PaimonSplitSerializerTest.java
index 3f72cea55..e4c4c45b8 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/source/PaimonSplitSerializerTest.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/source/PaimonSplitSerializerTest.java
@@ -27,9 +27,11 @@ import org.apache.paimon.CoreOptions;
 import org.apache.paimon.Snapshot;
 import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.io.DataOutputViewStreamWrapper;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.utils.InstantiationUtil;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayInputStream;
@@ -49,61 +51,18 @@ class PaimonSplitSerializerTest extends 
PaimonSourceTestBase {
 
     @Test
     void testSerializeAndDeserialize() throws Exception {
-        // prepare paimon table
-        int bucketNum = 1;
-        TablePath tablePath = TablePath.of(DEFAULT_DB, DEFAULT_TABLE);
-        Schema.Builder builder =
-                Schema.newBuilder()
-                        .column("c1", DataTypes.INT())
-                        .column("c2", DataTypes.STRING())
-                        .column("c3", DataTypes.STRING());
-        builder.partitionKeys("c3");
-        builder.primaryKey("c1", "c3");
-        builder.option(CoreOptions.BUCKET.key(), String.valueOf(bucketNum));
-        createTable(tablePath, builder.build());
-        Table table = getTable(tablePath);
-
-        GenericRow record1 =
-                GenericRow.of(12, BinaryString.fromString("a"), 
BinaryString.fromString("A"));
-        writeRecord(tablePath, Collections.singletonList(record1));
-        Snapshot snapshot = table.latestSnapshot().get();
-
-        LakeSource<PaimonSplit> lakeSource = 
lakeStorage.createLakeSource(tablePath);
-        List<PaimonSplit> plan = lakeSource.createPlanner(snapshot::id).plan();
-
-        PaimonSplit originalPaimonSplit = plan.get(0);
+        PaimonSplit originalPaimonSplit = createStringPartitionSplit();
         byte[] serialized = serializer.serialize(originalPaimonSplit);
         PaimonSplit deserialized = 
serializer.deserialize(serializer.getVersion(), serialized);
 
         
assertThat(deserialized.dataSplit()).isEqualTo(originalPaimonSplit.dataSplit());
         
assertThat(deserialized.isBucketUnAware()).isEqualTo(originalPaimonSplit.isBucketUnAware());
+        
assertThat(deserialized.partition()).isEqualTo(originalPaimonSplit.partition());
     }
 
     @Test
     void testJavaSerializationRoundTrip() throws Exception {
-        // prepare paimon table
-        int bucketNum = 1;
-        TablePath tablePath = TablePath.of(DEFAULT_DB, DEFAULT_TABLE);
-        Schema.Builder builder =
-                Schema.newBuilder()
-                        .column("c1", DataTypes.INT())
-                        .column("c2", DataTypes.STRING())
-                        .column("c3", DataTypes.STRING());
-        builder.partitionKeys("c3");
-        builder.primaryKey("c1", "c3");
-        builder.option(CoreOptions.BUCKET.key(), String.valueOf(bucketNum));
-        createTable(tablePath, builder.build());
-        Table table = getTable(tablePath);
-
-        GenericRow record1 =
-                GenericRow.of(12, BinaryString.fromString("a"), 
BinaryString.fromString("A"));
-        writeRecord(tablePath, Collections.singletonList(record1));
-        Snapshot snapshot = table.latestSnapshot().get();
-
-        LakeSource<PaimonSplit> lakeSource = 
lakeStorage.createLakeSource(tablePath);
-        List<PaimonSplit> plan = lakeSource.createPlanner(snapshot::id).plan();
-
-        PaimonSplit original = plan.get(0);
+        PaimonSplit original = createStringPartitionSplit();
 
         // Serialize via Java serialization
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -123,10 +82,56 @@ class PaimonSplitSerializerTest extends 
PaimonSourceTestBase {
         }
     }
 
+    @Test
+    void testDeserializeVersion1PreservesStringPartition() throws Exception {
+        PaimonSplit original = createStringPartitionSplit();
+        byte[] serialized = serializeVersion1(original);
+
+        PaimonSplit deserialized = serializer.deserialize(1, serialized);
+
+        assertThat(deserialized.dataSplit()).isEqualTo(original.dataSplit());
+        
assertThat(deserialized.isBucketUnAware()).isEqualTo(original.isBucketUnAware());
+        
assertThat(deserialized.partition()).isEqualTo(Collections.singletonList("A"));
+    }
+
     @Test
     void testDeserializeWithInvalidData() {
         byte[] invalidData = "invalid".getBytes();
         assertThatThrownBy(() -> serializer.deserialize(1, invalidData))
                 .isInstanceOf(IOException.class);
     }
+
+    private PaimonSplit createStringPartitionSplit() throws Exception {
+        // prepare paimon table
+        int bucketNum = 1;
+        TablePath tablePath = TablePath.of(DEFAULT_DB, DEFAULT_TABLE);
+        Schema.Builder builder =
+                Schema.newBuilder()
+                        .column("c1", DataTypes.INT())
+                        .column("c2", DataTypes.STRING())
+                        .column("c3", DataTypes.STRING());
+        builder.partitionKeys("c3");
+        builder.primaryKey("c1", "c3");
+        builder.option(CoreOptions.BUCKET.key(), String.valueOf(bucketNum));
+        createTable(tablePath, builder.build());
+        Table table = getTable(tablePath);
+
+        GenericRow record1 =
+                GenericRow.of(12, BinaryString.fromString("a"), 
BinaryString.fromString("A"));
+        writeRecord(tablePath, Collections.singletonList(record1));
+        Snapshot snapshot = table.latestSnapshot().get();
+
+        LakeSource<PaimonSplit> lakeSource = 
lakeStorage.createLakeSource(tablePath);
+        List<PaimonSplit> plan = lakeSource.createPlanner(snapshot::id).plan();
+
+        return plan.get(0);
+    }
+
+    private byte[] serializeVersion1(PaimonSplit paimonSplit) throws 
IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        DataOutputViewStreamWrapper view = new 
DataOutputViewStreamWrapper(out);
+        InstantiationUtil.serializeObject(view, paimonSplit.dataSplit());
+        view.writeBoolean(paimonSplit.isBucketUnAware());
+        return out.toByteArray();
+    }
 }
diff --git 
a/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/source/PaimonSplitTest.java
 
b/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/source/PaimonSplitTest.java
index a81800374..9d5b5ee5d 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/source/PaimonSplitTest.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/source/PaimonSplitTest.java
@@ -35,6 +35,7 @@ import org.apache.paimon.table.source.Split;
 import org.apache.paimon.types.DataTypes;
 import org.junit.jupiter.api.Test;
 
+import java.time.LocalDate;
 import java.util.Collections;
 import java.util.List;
 
@@ -77,4 +78,33 @@ class PaimonSplitTest extends PaimonSourceTestBase {
         assertThat(actualSplit).isEqualTo(paimonSplit.dataSplit());
         assertThat(((DataSplit) 
actualSplit).bucket()).isEqualTo(paimonSplit.bucket());
     }
+
+    @Test
+    void testPaimonSplitWithDatePartition() throws Exception {
+        int bucketNum = 1;
+        TablePath tablePath = TablePath.of(DEFAULT_DB, 
"non_string_partition_table");
+        Schema.Builder builder =
+                Schema.newBuilder()
+                        .column("c1", DataTypes.INT())
+                        .column("c2", DataTypes.STRING())
+                        .column("dt", DataTypes.DATE());
+        builder.partitionKeys("dt");
+        builder.primaryKey("c1", "dt");
+        builder.option(CoreOptions.BUCKET.key(), String.valueOf(bucketNum));
+        createTable(tablePath, builder.build());
+        Table table = getTable(tablePath);
+
+        int epochDay = (int) LocalDate.of(2024, 3, 1).toEpochDay();
+        GenericRow record1 = GenericRow.of(12, BinaryString.fromString("a"), 
epochDay);
+        writeRecord(tablePath, Collections.singletonList(record1));
+        Snapshot snapshot = table.latestSnapshot().get();
+
+        LakeSource<PaimonSplit> lakeSource = 
lakeStorage.createLakeSource(tablePath);
+        List<PaimonSplit> paimonSplits = 
lakeSource.createPlanner(snapshot::id).plan();
+
+        // The DATE partition must be rendered in Fluss partition-name format 
("2024-03-01"),
+        // not read blindly via BinaryRow.getString which yields garbage for 
non-String columns.
+        PaimonSplit paimonSplit = paimonSplits.get(0);
+        
assertThat(paimonSplit.partition()).isEqualTo(Collections.singletonList("2024-03-01"));
+    }
 }
diff --git 
a/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/utils/PaimonConversionsTest.java
 
b/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/utils/PaimonConversionsTest.java
new file mode 100644
index 000000000..45188b261
--- /dev/null
+++ 
b/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/utils/PaimonConversionsTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.fluss.lake.paimon.utils;
+
+import org.apache.fluss.types.DataTypeRoot;
+import org.apache.fluss.utils.PartitionUtils;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryRowWriter;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.util.function.Consumer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Test for partition value conversion in {@link PaimonConversions}. */
+class PaimonConversionsTest {
+
+    /** Each type's lake-side string must equal the Fluss-side name ({@code 
convertValueOfType}). */
+    @Test
+    void testTypesMatchFlussName() {
+        int epochDay = (int) LocalDate.of(2024, 3, 1).toEpochDay();
+        int milliOfDay = ((12 * 60 + 34) * 60 + 56) * 1000 + 789;
+        long ms = 1709294096123L;
+        int nanos = 456000; // multiple of 1000 to survive microsecond 
precision
+        byte[] bytes = {1, 2, 3, (byte) 0xAB};
+
+        assertMatches(
+                DataTypes.BOOLEAN(), w -> w.writeBoolean(0, true), true, 
DataTypeRoot.BOOLEAN);
+        assertMatches(
+                DataTypes.TINYINT(), w -> w.writeByte(0, (byte) 7), (byte) 7, 
DataTypeRoot.TINYINT);
+        assertMatches(
+                DataTypes.SMALLINT(),
+                w -> w.writeShort(0, (short) 300),
+                (short) 300,
+                DataTypeRoot.SMALLINT);
+        assertMatches(DataTypes.INT(), w -> w.writeInt(0, 42), 42, 
DataTypeRoot.INTEGER);
+        assertMatches(DataTypes.BIGINT(), w -> w.writeLong(0, 123L), 123L, 
DataTypeRoot.BIGINT);
+        assertMatches(DataTypes.FLOAT(), w -> w.writeFloat(0, 1.5f), 1.5f, 
DataTypeRoot.FLOAT);
+        assertMatches(DataTypes.DOUBLE(), w -> w.writeDouble(0, 2.5d), 2.5d, 
DataTypeRoot.DOUBLE);
+        assertMatches(DataTypes.DATE(), w -> w.writeInt(0, epochDay), 
epochDay, DataTypeRoot.DATE);
+        assertMatches(
+                DataTypes.TIME(),
+                w -> w.writeInt(0, milliOfDay),
+                milliOfDay,
+                DataTypeRoot.TIME_WITHOUT_TIME_ZONE);
+        assertMatches(DataTypes.BYTES(), w -> w.writeBinary(0, bytes), bytes, 
DataTypeRoot.BYTES);
+        assertMatches(
+                DataTypes.TIMESTAMP(6),
+                w -> w.writeTimestamp(0, Timestamp.fromEpochMillis(ms, nanos), 
6),
+                org.apache.fluss.row.TimestampNtz.fromMillis(ms, nanos),
+                DataTypeRoot.TIMESTAMP_WITHOUT_TIME_ZONE);
+        assertMatches(
+                DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(6),
+                w -> w.writeTimestamp(0, Timestamp.fromEpochMillis(ms, nanos), 
6),
+                org.apache.fluss.row.TimestampLtz.fromEpochMillis(ms, nanos),
+                DataTypeRoot.TIMESTAMP_WITH_LOCAL_TIME_ZONE);
+    }
+
+    @Test
+    void testStringPartition() {
+        BinaryRow partition = new BinaryRow(1);
+        BinaryRowWriter writer = new BinaryRowWriter(partition);
+        writer.writeString(0, BinaryString.fromString("a"));
+        writer.complete();
+
+        assertThat(flussPartitionNames(partition, 
RowType.of(DataTypes.STRING())))
+                .containsExactly("a");
+    }
+
+    @Test
+    void testMultiColumnPartition() {
+        int epochDay = (int) LocalDate.of(2024, 3, 1).toEpochDay();
+        BinaryRow partition = new BinaryRow(2);
+        BinaryRowWriter writer = new BinaryRowWriter(partition);
+        writer.writeInt(0, epochDay);
+        writer.writeInt(1, 42);
+        writer.complete();
+
+        assertThat(flussPartitionNames(partition, RowType.of(DataTypes.DATE(), 
DataTypes.INT())))
+                .containsExactly("2024-03-01", "42");
+    }
+
+    @Test
+    void testNestedTypeConversion() {
+        RowType paimon =
+                RowType.of(
+                        DataTypes.ARRAY(DataTypes.INT()),
+                        DataTypes.MAP(DataTypes.STRING(), DataTypes.BIGINT()),
+                        RowType.of(DataTypes.INT(), DataTypes.STRING()));
+        org.apache.fluss.types.RowType fluss = 
PaimonConversions.toFlussRowType(paimon);
+
+        
assertThat(fluss.getTypeAt(0).getTypeRoot()).isEqualTo(DataTypeRoot.ARRAY);
+        assertThat(
+                        ((org.apache.fluss.types.ArrayType) fluss.getTypeAt(0))
+                                .getElementType()
+                                .getTypeRoot())
+                .isEqualTo(DataTypeRoot.INTEGER);
+        
assertThat(fluss.getTypeAt(1).getTypeRoot()).isEqualTo(DataTypeRoot.MAP);
+        
assertThat(fluss.getTypeAt(2).getTypeRoot()).isEqualTo(DataTypeRoot.ROW);
+    }
+
+    /** Test helper: takes a Paimon partition type and runs the full Paimon -> 
Fluss conversion. */
+    private static java.util.List<String> flussPartitionNames(
+            BinaryRow partition, RowType paimonPartitionType) {
+        return PaimonConversions.toFlussPartitionValues(
+                partition, 
PaimonConversions.toFlussRowType(paimonPartitionType));
+    }
+
+    private static void assertMatches(
+            DataType paimonType,
+            Consumer<BinaryRowWriter> writeValue,
+            Object flussValue,
+            DataTypeRoot flussRoot) {
+        BinaryRow partition = new BinaryRow(1);
+        BinaryRowWriter writer = new BinaryRowWriter(partition);
+        writeValue.accept(writer);
+        writer.complete();
+        assertThat(flussPartitionNames(partition, RowType.of(paimonType)))
+                .as("type %s", flussRoot)
+                .containsExactly(PartitionUtils.convertValueOfType(flussValue, 
flussRoot));
+    }
+}


Reply via email to