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

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 7b35dcf7fdd branch-4.1: [fix](paimon) Handle special characters in 
partition values #65904 (#65938)
7b35dcf7fdd is described below

commit 7b35dcf7fdd49d131d640d1f0a4d05591153e1a2
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Thu Jul 23 20:19:02 2026 +0800

    branch-4.1: [fix](paimon) Handle special characters in partition values 
#65904 (#65938)
    
    Cherry-picked from #65904
    
    Co-authored-by: zhangstar333 <[email protected]>
---
 .../apache/doris/datasource/paimon/PaimonUtil.java |  48 ++++----
 .../doris/datasource/paimon/PaimonUtilTest.java    | 123 +++++++++++++++++++++
 2 files changed, 149 insertions(+), 22 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
index 1c71ac3dd24..ea49e80a5b9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/paimon/PaimonUtil.java
@@ -29,7 +29,6 @@ import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.UserException;
 import org.apache.doris.common.util.TimeUtils;
 import org.apache.doris.datasource.ExternalTable;
-import org.apache.doris.datasource.hive.HiveUtil;
 import org.apache.doris.thrift.TColumnType;
 import org.apache.doris.thrift.TPrimitiveType;
 import org.apache.doris.thrift.schema.external.TArrayField;
@@ -77,6 +76,7 @@ import org.apache.paimon.types.VarCharType;
 import org.apache.paimon.utils.DateTimeUtils;
 import org.apache.paimon.utils.InstantiationUtil;
 import org.apache.paimon.utils.Pair;
+import org.apache.paimon.utils.PartitionPathUtils;
 import org.apache.paimon.utils.Projection;
 import org.apache.paimon.utils.RowDataToObjectArrayConverter;
 
@@ -91,6 +91,7 @@ import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Base64;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -162,47 +163,50 @@ public class PaimonUtil {
         List<Type> types = partitionColumns.stream()
                 .map(Column::getType)
                 .collect(Collectors.toList());
-        Map<String, Type> columnNameToType = partitionColumns.stream()
-                .collect(Collectors.toMap(Column::getName, Column::getType));
 
         for (Partition partition : paimonPartitions) {
             Map<String, String> spec = partition.spec();
-            StringBuilder sb = new StringBuilder();
-            for (Map.Entry<String, String> entry : spec.entrySet()) {
-                sb.append(entry.getKey()).append("=");
+            // Paimon partition specs contain logical values, which may 
include path separators.
+            // Build partition values directly instead of parsing them as a 
Hive partition path.
+            List<String> partitionValues = 
Lists.newArrayListWithExpectedSize(partitionColumns.size());
+            LinkedHashMap<String, String> orderedPartitionSpec = new 
LinkedHashMap<>();
+            for (Column partitionColumn : partitionColumns) {
+                String partitionColumnName = partitionColumn.getName();
+                String partitionValue = spec.get(partitionColumnName);
                 // When partition.legacy-name = true (default), Paimon stores 
DATE type as days since
                 // 1970-01-01 (epoch integer), so we need to convert the 
integer to a date string.
                 // When partition.legacy-name = false, the value is already a 
human read date string.
-                if (legacyPartitionName
-                        && columnNameToType.getOrDefault(entry.getKey(), 
Type.NULL).isDateV2()) {
-                    
sb.append(DateTimeUtils.formatDate(Integer.parseInt(entry.getValue()))).append("/");
-                } else {
-                    sb.append(entry.getValue()).append("/");
+                if (legacyPartitionName && 
partitionColumn.getType().isDateV2()) {
+                    partitionValue = 
DateTimeUtils.formatDate(Integer.parseInt(partitionValue));
                 }
+                partitionValues.add(partitionValue);
+                orderedPartitionSpec.put(partitionColumnName, partitionValue);
             }
-            if (sb.length() > 0) {
-                sb.deleteCharAt(sb.length() - 1);
-            }
-            String partitionName = sb.toString();
-            nameToPartition.put(partitionName, partition);
+            String partitionPath = 
PartitionPathUtils.generatePartitionPath(orderedPartitionSpec);
+            String partitionName = partitionPath.substring(0, 
partitionPath.length() - 1);
+            Partition previousPartition = 
nameToPartition.putIfAbsent(partitionName, partition);
+            Preconditions.checkState(previousPartition == null,
+                    "Duplicate Paimon partition name: " + partitionName);
+            PartitionItem partitionItem;
             try {
                 // partition values return by paimon api, may have problem,
                 // to avoid affecting the query, we catch exceptions here
-                nameToPartitionItem.put(partitionName, 
toListPartitionItem(partitionName, types));
+                partitionItem = toListPartitionItem(partitionValues, types);
             } catch (Exception e) {
                 LOG.warn("toListPartitionItem failed, partitionColumns: {}, 
partitionValues: {}",
                         partitionColumns, partition.spec(), e);
+                continue;
             }
+            PartitionItem previousPartitionItem = 
nameToPartitionItem.putIfAbsent(partitionName, partitionItem);
+            Preconditions.checkState(previousPartitionItem == null,
+                    "Duplicate Paimon partition item name: " + partitionName);
         }
         return partitionInfo;
     }
 
-    public static ListPartitionItem toListPartitionItem(String partitionName, 
List<Type> types)
+    public static ListPartitionItem toListPartitionItem(List<String> 
partitionValues, List<Type> types)
             throws AnalysisException {
-        // Partition name will be in format: nation=cn/city=beijing
-        // parse it to get values "cn" and "beijing"
-        List<String> partitionValues = 
HiveUtil.toPartitionValues(partitionName);
-        Preconditions.checkState(partitionValues.size() == types.size(), 
partitionName + " vs. " + types);
+        Preconditions.checkState(partitionValues.size() == types.size(), 
partitionValues + " vs. " + types);
         List<PartitionValue> values = 
Lists.newArrayListWithExpectedSize(types.size());
         for (String partitionValue : partitionValues) {
             // null  will in partition 'null'
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
index 93cbba81cb7..fe4c94f2f1b 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/paimon/PaimonUtilTest.java
@@ -18,6 +18,8 @@
 package org.apache.doris.datasource.paimon;
 
 import org.apache.doris.catalog.Column;
+import org.apache.doris.catalog.ListPartitionItem;
+import org.apache.doris.catalog.PartitionItem;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.thrift.TPrimitiveType;
 import org.apache.doris.thrift.schema.external.TFieldPtr;
@@ -26,6 +28,7 @@ import org.apache.doris.thrift.schema.external.TSchema;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.BinaryRowWriter;
 import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.partition.Partition;
 import org.apache.paimon.schema.TableSchema;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.types.CharType;
@@ -39,6 +42,7 @@ import org.mockito.Mockito;
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -128,6 +132,125 @@ public class PaimonUtilTest {
         Assert.assertEquals("2026-05-26", partitionInfoMap.get("Dt"));
     }
 
+    @Test
+    public void testGeneratePartitionInfoWithSpecialCharacters() {
+        List<Column> partitionColumns = Arrays.asList(
+                new Column("source", Type.STRING),
+                new Column("part_str", Type.STRING),
+                new Column("pass", Type.STRING));
+        Map<String, String> spec = new LinkedHashMap<>();
+        spec.put("source", "dataset/team-a/segment-01");
+        spec.put("part_str", "/ymd=20260701/hour=[0-9][0-9]/*.jsonl");
+        spec.put("pass", "s1");
+        Partition partition = new Partition(spec, 1L, 1L, 1L, 1L, false);
+
+        PaimonPartitionInfo partitionInfo = PaimonUtil.generatePartitionInfo(
+                partitionColumns, Collections.singletonList(partition), false);
+
+        Assert.assertFalse(partitionInfo.isPartitionInvalid());
+        Assert.assertEquals(1, partitionInfo.getNameToPartition().size());
+        Assert.assertEquals(1, partitionInfo.getNameToPartitionItem().size());
+        String partitionName = "source=dataset%2Fteam-a%2Fsegment-01"
+                + 
"/part_str=%2Fymd%3D20260701%2Fhour%3D%5B0-9%5D%5B0-9%5D%2F%2A.jsonl/pass=s1";
+        
Assert.assertTrue(partitionInfo.getNameToPartition().containsKey(partitionName));
+        PartitionItem partitionItem = 
partitionInfo.getNameToPartitionItem().values().iterator().next();
+        List<String> actualValues = ((ListPartitionItem) 
partitionItem).getItems().get(0)
+                .getPartitionValuesAsStringList();
+        Assert.assertEquals(Arrays.asList(
+                "dataset/team-a/segment-01",
+                "/ymd=20260701/hour=[0-9][0-9]/*.jsonl",
+                "s1"), actualValues);
+    }
+
+    @Test
+    public void testGeneratePartitionInfoUsesPartitionColumnOrder() {
+        List<Column> partitionColumns = Arrays.asList(
+                new Column("source", Type.STRING),
+                new Column("part_str", Type.STRING),
+                new Column("pass", Type.STRING));
+        Map<String, String> spec = new LinkedHashMap<>();
+        spec.put("pass", "s1");
+        spec.put("part_str", "/ymd=20260721");
+        spec.put("source", "dataset/team-a/segment-01");
+        Partition partition = new Partition(spec, 1L, 1L, 1L, 1L, false);
+
+        PaimonPartitionInfo partitionInfo = PaimonUtil.generatePartitionInfo(
+                partitionColumns, Collections.singletonList(partition), false);
+
+        String partitionName = "source=dataset%2Fteam-a%2Fsegment-01"
+                + "/part_str=%2Fymd%3D20260721/pass=s1";
+        
Assert.assertTrue(partitionInfo.getNameToPartition().containsKey(partitionName));
+        PartitionItem partitionItem = 
partitionInfo.getNameToPartitionItem().get(partitionName);
+        List<String> actualValues = ((ListPartitionItem) 
partitionItem).getItems().get(0)
+                .getPartitionValuesAsStringList();
+        Assert.assertEquals(Arrays.asList(
+                "dataset/team-a/segment-01", "/ymd=20260721", "s1"), 
actualValues);
+    }
+
+    @Test
+    public void testGeneratePartitionInfoPreservesLegacyDateConversion() {
+        List<Column> partitionColumns = Collections.singletonList(new 
Column("dt", Type.DATEV2));
+        Map<String, String> spec = new LinkedHashMap<>();
+        spec.put("dt", "19737");
+        Partition partition = new Partition(spec, 1L, 1L, 1L, 1L, false);
+
+        PaimonPartitionInfo partitionInfo = PaimonUtil.generatePartitionInfo(
+                partitionColumns, Collections.singletonList(partition), true);
+
+        String partitionName = "dt=2024-01-15";
+        
Assert.assertTrue(partitionInfo.getNameToPartition().containsKey(partitionName));
+        PartitionItem partitionItem = 
partitionInfo.getNameToPartitionItem().get(partitionName);
+        Assert.assertEquals(Collections.singletonList("2024-01-15"),
+                ((ListPartitionItem) 
partitionItem).getItems().get(0).getPartitionValuesAsStringList());
+    }
+
+    @Test
+    public void testGeneratePartitionInfoUsesCollisionFreePartitionNames() {
+        List<Column> partitionColumns = Arrays.asList(
+                new Column("a", Type.STRING),
+                new Column("b", Type.STRING));
+        Map<String, String> firstSpec = new LinkedHashMap<>();
+        firstSpec.put("a", "x/b=y");
+        firstSpec.put("b", "z");
+        Map<String, String> secondSpec = new LinkedHashMap<>();
+        secondSpec.put("a", "x");
+        secondSpec.put("b", "y/b=z");
+        Partition firstPartition = new Partition(firstSpec, 1L, 1L, 1L, 1L, 
false);
+        Partition secondPartition = new Partition(secondSpec, 2L, 2L, 2L, 2L, 
false);
+
+        PaimonPartitionInfo partitionInfo = PaimonUtil.generatePartitionInfo(
+                partitionColumns, Arrays.asList(firstPartition, 
secondPartition), false);
+
+        String firstPartitionName = "a=x%2Fb%3Dy/b=z";
+        String secondPartitionName = "a=x/b=y%2Fb%3Dz";
+        Assert.assertFalse(partitionInfo.isPartitionInvalid());
+        Assert.assertEquals(2, partitionInfo.getNameToPartition().size());
+        Assert.assertEquals(2, partitionInfo.getNameToPartitionItem().size());
+        Assert.assertSame(firstPartition, 
partitionInfo.getNameToPartition().get(firstPartitionName));
+        Assert.assertSame(secondPartition, 
partitionInfo.getNameToPartition().get(secondPartitionName));
+        Assert.assertEquals(Arrays.asList("x/b=y", "z"),
+                ((ListPartitionItem) 
partitionInfo.getNameToPartitionItem().get(firstPartitionName))
+                        .getItems().get(0).getPartitionValuesAsStringList());
+        Assert.assertEquals(Arrays.asList("x", "y/b=z"),
+                ((ListPartitionItem) 
partitionInfo.getNameToPartitionItem().get(secondPartitionName))
+                        .getItems().get(0).getPartitionValuesAsStringList());
+    }
+
+    @Test
+    public void testGeneratePartitionInfoRejectsDuplicatePartitionNames() {
+        List<Column> partitionColumns = Collections.singletonList(new 
Column("part", Type.STRING));
+        Map<String, String> firstSpec = Collections.singletonMap("part", 
"same");
+        Map<String, String> secondSpec = Collections.singletonMap("part", 
"same");
+        Partition firstPartition = new Partition(firstSpec, 1L, 1L, 1L, 1L, 
false);
+        Partition secondPartition = new Partition(secondSpec, 2L, 2L, 2L, 2L, 
false);
+
+        IllegalStateException exception = 
Assert.assertThrows(IllegalStateException.class,
+                () -> PaimonUtil.generatePartitionInfo(
+                        partitionColumns, Arrays.asList(firstPartition, 
secondPartition), false));
+
+        Assert.assertTrue(exception.getMessage().contains("Duplicate Paimon 
partition name"));
+    }
+
     @Test
     public void testBinlogHistorySchemaWithSequenceNumber() {
         PaimonSysExternalTable binlogTable = 
Mockito.mock(PaimonSysExternalTable.class);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to