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

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


The following commit(s) were added to refs/heads/master by this push:
     new c12606b203 [spark] Keep leading zeros in string partition values of 
procedure partitions (#10738)
c12606b203 is described below

commit c12606b203de692db9d9423195d3a0c2cf1698e0
Author: Eunbin Son <[email protected]>
AuthorDate: Mon Oct 26 13:21:31 2026 +0900

    [spark] Keep leading zeros in string partition values of procedure 
partitions (#10738)
---
 .../paimon/spark/utils/SparkProcedureUtils.java    | 41 ++++++++++++++++-----
 .../spark/procedure/CompactProcedureTestBase.scala | 43 ++++++++++++++++++++++
 2 files changed, 75 insertions(+), 9 deletions(-)

diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/utils/SparkProcedureUtils.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/utils/SparkProcedureUtils.java
index 923c8319df..104477ac2e 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/utils/SparkProcedureUtils.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/utils/SparkProcedureUtils.java
@@ -23,6 +23,7 @@ import org.apache.paimon.predicate.Predicate;
 import org.apache.paimon.predicate.PredicateBuilder;
 import org.apache.paimon.spark.catalyst.analysis.expressions.ExpressionUtils;
 import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.types.DataTypeFamily;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.ParameterUtils;
 import org.apache.paimon.utils.StringUtils;
@@ -32,6 +33,7 @@ import org.apache.spark.sql.catalyst.expressions.Expression;
 import org.apache.spark.sql.catalyst.expressions.Literal;
 import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan;
 import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation;
+import org.apache.spark.sql.types.StringType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -88,7 +90,8 @@ public class SparkProcedureUtils {
         // `dt=2024-01-01,hh=0;dt=2024-01-02,hh=1`, not a SQL expression.
         // Values that are valid Spark SQL literals are normalized with 
Spark's parser, so quoted
         // partition values follow Spark partition spec behavior. Values that 
are not literals are
-        // kept as path-style typed strings for backward compatibility.
+        // kept as path-style typed strings for backward compatibility. For 
string partitions only
+        // quoted values are unquoted; other values are kept as written (e.g. 
`hh=08`).
         //
         // Invalid partition keys are rejected explicitly here so procedures 
can fail with a clear
         // error message before converting values to Paimon internal literals.
@@ -105,7 +108,8 @@ public class SparkProcedureUtils {
         List<Map<String, String>> partitionSpecs =
                 ParameterUtils.getPartitions(partitions.split(";"));
         validatePartitionKeys(partitionSpecs, partitionKeys);
-        partitionSpecs = normalizePartitionValuesWithSparkParser(spark, 
partitionSpecs);
+        partitionSpecs =
+                normalizePartitionValuesWithSparkParser(spark, partitionSpecs, 
partitionType);
 
         Predicate predicate =
                 PredicateBuilder.partitions(
@@ -132,9 +136,11 @@ public class SparkProcedureUtils {
     }
 
     private static List<Map<String, String>> 
normalizePartitionValuesWithSparkParser(
-            SparkSession spark, List<Map<String, String>> partitionSpecs) {
+            SparkSession spark, List<Map<String, String>> partitionSpecs, 
RowType partitionType) {
         return partitionSpecs.stream()
-                .map(partitionSpec -> parsePartitionSpec(spark, partitionSpec, 
false))
+                .map(
+                        partitionSpec ->
+                                parsePartitionSpec(spark, partitionSpec, 
false, partitionType))
                 .collect(java.util.stream.Collectors.toList());
     }
 
@@ -149,21 +155,32 @@ public class SparkProcedureUtils {
      */
     public static Map<String, String> parseStaticPartition(
             SparkSession spark, Map<String, String> partitionSpec) {
-        return parsePartitionSpec(spark, partitionSpec, true);
+        return parsePartitionSpec(spark, partitionSpec, true, null);
     }
 
     private static Map<String, String> parsePartitionSpec(
-            SparkSession spark, Map<String, String> partitionSpec, boolean 
requireLiteral) {
+            SparkSession spark,
+            Map<String, String> partitionSpec,
+            boolean requireLiteral,
+            @Nullable RowType partitionType) {
         Map<String, String> staticPartition = new LinkedHashMap<>();
         for (Map.Entry<String, String> entry : partitionSpec.entrySet()) {
+            boolean isStringPartition =
+                    partitionType != null
+                            && partitionType
+                                    .getField(entry.getKey())
+                                    .type()
+                                    .is(DataTypeFamily.CHARACTER_STRING);
             staticPartition.put(
-                    entry.getKey(), parsePartitionValue(spark, 
entry.getValue(), requireLiteral));
+                    entry.getKey(),
+                    parsePartitionValue(
+                            spark, entry.getValue(), requireLiteral, 
isStringPartition));
         }
         return staticPartition;
     }
 
     private static String parsePartitionValue(
-            SparkSession spark, String value, boolean requireLiteral) {
+            SparkSession spark, String value, boolean requireLiteral, boolean 
isStringPartition) {
         Expression expr;
         try {
             expr = spark.sessionState().sqlParser().parseExpression(value);
@@ -181,11 +198,17 @@ public class SparkProcedureUtils {
             return value;
         }
 
-        Object literalValue = ((Literal) expr).value();
+        Literal literal = (Literal) expr;
+        Object literalValue = literal.value();
         if (literalValue == null) {
             checkArgument(!requireLiteral, "Partition value cannot be null");
             return value;
         }
+        if (isStringPartition && !(literal.dataType() instanceof StringType)) {
+            // Keep the raw text of a string partition value: parsing `08` as 
a numeric literal
+            // would turn it into `8` and target another partition.
+            return value;
+        }
         return literalValue.toString();
     }
 
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactProcedureTestBase.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactProcedureTestBase.scala
index 9364ad824b..8a64696ab2 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactProcedureTestBase.scala
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/CompactProcedureTestBase.scala
@@ -1803,6 +1803,49 @@ abstract class CompactProcedureTestBase extends 
PaimonSparkTestBase with StreamT
     }
   }
 
+  test("Paimon Procedure: compact partitions keep leading zeros of string 
values") {
+    def checkCompactedPartition(
+        hhType: String,
+        hhValues: Seq[String],
+        partitions: String,
+        expectedFileCounts: Seq[Row]): Unit = {
+      withTable("T") {
+        spark.sql(s"""
+                     |CREATE TABLE T (id INT, hh $hhType)
+                     |TBLPROPERTIES ('bucket'='-1', 'write-only'='true')
+                     |PARTITIONED BY (hh)
+                     |""".stripMargin)
+        hhValues.foreach {
+          hh =>
+            spark.sql(s"INSERT INTO T VALUES (1, $hh)")
+            spark.sql(s"INSERT INTO T VALUES (2, $hh)")
+        }
+        checkAnswer(
+          spark.sql(
+            s"CALL sys.compact(table => 'T', partitions => '$partitions', 
options => 'compaction.min.file-num=2')"),
+          Row(true) :: Nil)
+        checkAnswer(
+          spark.sql("SELECT partition, count(*) FROM `T$files` GROUP BY 
partition"),
+          expectedFileCounts)
+      }
+    }
+
+    // an unquoted value of a string partition is kept as written
+    checkCompactedPartition(
+      "STRING",
+      Seq("'08'", "'8'"),
+      "hh=08",
+      Row("{08}", 1L) :: Row("{8}", 2L) :: Nil)
+    // a quoted value of a string partition is still unquoted
+    checkCompactedPartition(
+      "STRING",
+      Seq("'08'", "'8'"),
+      "hh=\"08\"",
+      Row("{08}", 1L) :: Row("{8}", 2L) :: Nil)
+    // a numeric partition still matches by value
+    checkCompactedPartition("INT", Seq("8", "9"), "hh=08", Row("{8}", 1L) :: 
Row("{9}", 2L) :: Nil)
+  }
+
   test("Paimon Procedure: compact with invalid partition key") {
     withTable("T") {
       spark.sql(s"""

Reply via email to