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 f26a23fdba [spark] Fix partition predicate conversion in procedures 
(#8390)
f26a23fdba is described below

commit f26a23fdbacf76fdab47edffdbadde94fb49f286
Author: huangxiaoping <[email protected]>
AuthorDate: Tue Jun 30 17:20:15 2026 +0800

    [spark] Fix partition predicate conversion in procedures (#8390)
---
 .../paimon/codegen/CodeGeneratorContext.scala      |  11 +-
 .../procedure/CompactChainTableProcedure.java      |  19 +--
 .../paimon/spark/procedure/CompactProcedure.java   |  20 ++-
 .../procedure/CreateGlobalIndexProcedure.java      |  12 +-
 .../spark/procedure/DropGlobalIndexProcedure.java  |  11 +-
 .../paimon/spark/procedure/RescaleProcedure.java   |  21 +--
 .../paimon/spark/utils/SparkProcedureUtils.java    | 143 ++++++++++++++++-----
 .../spark/procedure/CompactProcedureTestBase.scala |  41 +++++-
 .../spark/procedure/RescaleProcedureTest.scala     |  31 +++++
 9 files changed, 230 insertions(+), 79 deletions(-)

diff --git 
a/paimon-codegen/src/main/scala/org/apache/paimon/codegen/CodeGeneratorContext.scala
 
b/paimon-codegen/src/main/scala/org/apache/paimon/codegen/CodeGeneratorContext.scala
index 0c33c8a6e6..5f137ea25e 100644
--- 
a/paimon-codegen/src/main/scala/org/apache/paimon/codegen/CodeGeneratorContext.scala
+++ 
b/paimon-codegen/src/main/scala/org/apache/paimon/codegen/CodeGeneratorContext.scala
@@ -57,8 +57,9 @@ class CodeGeneratorContext {
   private var currentMethodNameForLocalVariables = "DEFAULT"
 
   // method_name -> local_variable_statements
-  private val reusableLocalVariableStatements = mutable.Map[String, 
mutable.LinkedHashSet[String]](
-    (currentMethodNameForLocalVariables, mutable.LinkedHashSet[String]()))
+  private val reusableLocalVariableStatements = mutable.Map[String, 
mutable.LinkedHashSet[String]]()
+  reusableLocalVariableStatements(currentMethodNameForLocalVariables) =
+    new mutable.LinkedHashSet[String]()
 
   /**
    * Starts a new local variable statements for a generated class with the 
given method name.
@@ -68,7 +69,7 @@ class CodeGeneratorContext {
    */
   def startNewLocalVariableStatement(methodName: String): Unit = {
     currentMethodNameForLocalVariables = methodName
-    reusableLocalVariableStatements(methodName) = 
mutable.LinkedHashSet[String]()
+    reusableLocalVariableStatements(methodName) = new 
mutable.LinkedHashSet[String]()
   }
 
   /**
@@ -86,7 +87,7 @@ class CodeGeneratorContext {
   def addReusableLocalVariable(fieldTypeTerm: String, fieldName: String): 
String = {
     val fieldTerm = newName(fieldName)
     reusableLocalVariableStatements
-      .getOrElse(currentMethodNameForLocalVariables, 
mutable.LinkedHashSet[String]())
+      .getOrElse(currentMethodNameForLocalVariables, new 
mutable.LinkedHashSet[String]())
       .add(s"$fieldTypeTerm $fieldTerm;")
     fieldTerm
   }
@@ -105,7 +106,7 @@ class CodeGeneratorContext {
     fieldTypeAndNames.map(_._1).zip(fieldTerms).foreach {
       case (fieldTypeTerm, fieldTerm) =>
         reusableLocalVariableStatements
-          .getOrElse(currentMethodNameForLocalVariables, 
mutable.LinkedHashSet[String]())
+          .getOrElse(currentMethodNameForLocalVariables, new 
mutable.LinkedHashSet[String]())
           .add(s"$fieldTypeTerm $fieldTerm;")
     }
     fieldTerms
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactChainTableProcedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactChainTableProcedure.java
index ad992cf505..6594dabd38 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactChainTableProcedure.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactChainTableProcedure.java
@@ -132,21 +132,20 @@ public class CompactChainTableProcedure extends 
BaseProcedure {
             DataSourceV2Relation relation,
             String partitionStr,
             boolean overwrite) {
-        String partition = SparkProcedureUtils.toWhere(partitionStr);
         FileStoreTable snapshotTable = table.wrapped();
 
         ChainGroupReadTable.ChainTableBatchScan scan =
                 (ChainGroupReadTable.ChainTableBatchScan) table.newScan();
         PartitionPredicate partitionPredicate =
-                SparkProcedureUtils.convertToPartitionPredicate(
-                        partition, table.schema().logicalPartitionType(), 
spark(), relation);
+                SparkProcedureUtils.convertPartitionsToPartitionPredicate(
+                        partitionStr, snapshotTable, spark());
 
         // Check if target partition already exists in snapshot branch
-        boolean partitionExists = checkPartitionExists(snapshotTable, 
partition, relation);
+        boolean partitionExists = checkPartitionExists(snapshotTable, 
partitionStr);
         if (partitionExists) {
             if (overwrite) {
                 
scan.skipPreloadTargetSnapshot().withPartitionFilter(partitionPredicate);
-                LOG.info("Found existing partition {}, will overwrite it.", 
partition);
+                LOG.info("Found existing partition {}, will overwrite it.", 
partitionStr);
             } else {
                 LOG.info(
                         "Partition {} already exists in snapshot branch, 
skipping compaction.",
@@ -187,14 +186,10 @@ public class CompactChainTableProcedure extends 
BaseProcedure {
         return true;
     }
 
-    private boolean checkPartitionExists(
-            FileStoreTable snapshotTable, String partition, 
DataSourceV2Relation relation) {
+    private boolean checkPartitionExists(FileStoreTable snapshotTable, String 
partition) {
         PartitionPredicate snapshotPartitionPredicate =
-                SparkProcedureUtils.convertToPartitionPredicate(
-                        partition,
-                        snapshotTable.schema().logicalPartitionType(),
-                        spark(),
-                        relation);
+                SparkProcedureUtils.convertPartitionsToPartitionPredicate(
+                        partition, snapshotTable, spark());
 
         return !snapshotTable
                 .newScan()
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactProcedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactProcedure.java
index b07fbd26be..6854633b74 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactProcedure.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CompactProcedure.java
@@ -183,7 +183,6 @@ public class CompactProcedure extends BaseProcedure {
         checkArgument(
                 partitions == null || where == null,
                 "partitions and where cannot be used together.");
-        String finalWhere = partitions != null ? 
SparkProcedureUtils.toWhere(partitions) : where;
         return modifySparkTable(
                 tableIdent,
                 sparkTable -> {
@@ -197,12 +196,19 @@ public class CompactProcedure extends BaseProcedure {
                             sortColumns,
                             table.partitionKeys());
                     DataSourceV2Relation relation = createRelation(tableIdent, 
sparkTable);
-                    PartitionPredicate partitionPredicate =
-                            SparkProcedureUtils.convertToPartitionPredicate(
-                                    finalWhere,
-                                    table.schema().logicalPartitionType(),
-                                    spark(),
-                                    relation);
+                    PartitionPredicate partitionPredicate;
+                    if (partitions != null) {
+                        partitionPredicate =
+                                
SparkProcedureUtils.convertPartitionsToPartitionPredicate(
+                                        partitions, table, spark());
+                    } else {
+                        partitionPredicate =
+                                
SparkProcedureUtils.convertToPartitionPredicate(
+                                        where,
+                                        table.schema().logicalPartitionType(),
+                                        spark(),
+                                        relation);
+                    }
                     HashMap<String, String> dynamicOptions = new HashMap<>();
                     ProcedureUtils.putIfNotEmpty(
                             dynamicOptions, CoreOptions.WRITE_ONLY.key(), 
"false");
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CreateGlobalIndexProcedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CreateGlobalIndexProcedure.java
index adceb783af..cff2fece60 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CreateGlobalIndexProcedure.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/CreateGlobalIndexProcedure.java
@@ -116,9 +116,8 @@ public class CreateGlobalIndexProcedure extends 
BaseProcedure {
                         : args.getString(3);
         String optionString = args.isNullAt(4) ? null : args.getString(4);
 
-        String finalWhere = partitions != null ? 
SparkProcedureUtils.toWhere(partitions) : null;
-
-        LOG.info("Starting to build index for table " + tableIdent + " WHERE: 
" + finalWhere);
+        LOG.info(
+                "Starting to build index for table {} with partitions: {}", 
tableIdent, partitions);
 
         return modifySparkTable(
                 tableIdent,
@@ -160,11 +159,8 @@ public class CreateGlobalIndexProcedure extends 
BaseProcedure {
                         }
                         DataSourceV2Relation relation = 
createRelation(tableIdent, sparkTable);
                         PartitionPredicate partitionPredicate =
-                                
SparkProcedureUtils.convertToPartitionPredicate(
-                                        finalWhere,
-                                        table.schema().logicalPartitionType(),
-                                        spark(),
-                                        relation);
+                                
SparkProcedureUtils.convertPartitionsToPartitionPredicate(
+                                        partitions, table, spark());
 
                         List<DataField> indexFields =
                                 indexColumns.stream()
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/DropGlobalIndexProcedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/DropGlobalIndexProcedure.java
index ec8b1a2c85..3aae599186 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/DropGlobalIndexProcedure.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/DropGlobalIndexProcedure.java
@@ -108,9 +108,7 @@ public class DropGlobalIndexProcedure extends BaseProcedure 
{
                         : args.getString(3);
         boolean dryRun = !args.isNullAt(4) && args.getBoolean(4);
 
-        String finalWhere = partitions != null ? 
SparkProcedureUtils.toWhere(partitions) : null;
-
-        LOG.info("Starting to drop index for table " + tableIdent + " WHERE: " 
+ finalWhere);
+        LOG.info("Starting to drop index for table {} with partitions: {}", 
tableIdent, partitions);
 
         List<String> indexColumns =
                 Arrays.stream(column.split(","))
@@ -142,11 +140,8 @@ public class DropGlobalIndexProcedure extends 
BaseProcedure {
                                         .collect(Collectors.toList());
                         DataSourceV2Relation relation = 
createRelation(tableIdent);
                         PartitionPredicate partitionPredicate =
-                                
SparkProcedureUtils.convertToPartitionPredicate(
-                                        finalWhere,
-                                        table.schema().logicalPartitionType(),
-                                        spark(),
-                                        relation);
+                                
SparkProcedureUtils.convertPartitionsToPartitionPredicate(
+                                        partitions, table, spark());
 
                         Snapshot snapshot =
                                 t.latestSnapshot()
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RescaleProcedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RescaleProcedure.java
index 4a4c15ceff..404a78db78 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RescaleProcedure.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/RescaleProcedure.java
@@ -104,8 +104,6 @@ public class RescaleProcedure extends BaseProcedure {
         checkArgument(
                 partitions == null || where == null,
                 "partitions and where cannot be used together.");
-        String finalWhere = partitions != null ? 
SparkProcedureUtils.toWhere(partitions) : where;
-
         return modifySparkTable(
                 tableIdent,
                 sparkTable -> {
@@ -132,12 +130,19 @@ public class RescaleProcedure extends BaseProcedure {
                     fileStoreTable = fileStoreTable.copy(dynamicOptions);
 
                     DataSourceV2Relation relation = createRelation(tableIdent, 
sparkTable);
-                    PartitionPredicate partitionPredicate =
-                            SparkProcedureUtils.convertToPartitionPredicate(
-                                    finalWhere,
-                                    
fileStoreTable.schema().logicalPartitionType(),
-                                    spark(),
-                                    relation);
+                    PartitionPredicate partitionPredicate;
+                    if (partitions != null) {
+                        partitionPredicate =
+                                
SparkProcedureUtils.convertPartitionsToPartitionPredicate(
+                                        partitions, fileStoreTable, spark());
+                    } else {
+                        partitionPredicate =
+                                
SparkProcedureUtils.convertToPartitionPredicate(
+                                        where,
+                                        
fileStoreTable.schema().logicalPartitionType(),
+                                        spark(),
+                                        relation);
+                    }
 
                     if (bucketNum == null) {
                         checkArgument(
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 59f93f92fb..923c8319df 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
@@ -20,7 +20,9 @@ package org.apache.paimon.spark.utils;
 
 import org.apache.paimon.partition.PartitionPredicate;
 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.RowType;
 import org.apache.paimon.utils.ParameterUtils;
 import org.apache.paimon.utils.StringUtils;
@@ -35,10 +37,12 @@ import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nullable;
 
-import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
 import static org.apache.paimon.utils.Preconditions.checkArgument;
 
@@ -77,6 +81,114 @@ public class SparkProcedureUtils {
         return PartitionPredicate.fromPredicate(partitionType, predicate);
     }
 
+    @Nullable
+    public static PartitionPredicate convertPartitionsToPartitionPredicate(
+            @Nullable String partitions, FileStoreTable table, SparkSession 
spark) {
+        // `partitions` is a structured partition spec path such as
+        // `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.
+        //
+        // Invalid partition keys are rejected explicitly here so procedures 
can fail with a clear
+        // error message before converting values to Paimon internal literals.
+        if (StringUtils.isNullOrWhitespaceOnly(partitions)) {
+            return null;
+        }
+
+        RowType partitionType = table.schema().logicalPartitionType();
+        List<String> partitionKeys = partitionType.getFieldNames();
+        checkArgument(
+                !partitionKeys.isEmpty(),
+                "Table should be a partitioned table when using partition 
predicate.");
+
+        List<Map<String, String>> partitionSpecs =
+                ParameterUtils.getPartitions(partitions.split(";"));
+        validatePartitionKeys(partitionSpecs, partitionKeys);
+        partitionSpecs = normalizePartitionValuesWithSparkParser(spark, 
partitionSpecs);
+
+        Predicate predicate =
+                PredicateBuilder.partitions(
+                        partitionSpecs, partitionType, 
table.coreOptions().partitionDefaultName());
+        return PartitionPredicate.fromPredicate(partitionType, predicate);
+    }
+
+    private static void validatePartitionKeys(
+            List<Map<String, String>> partitionSpecs, List<String> 
partitionKeys) {
+        Set<String> invalidKeys = new HashSet<>();
+        for (Map<String, String> partitionSpec : partitionSpecs) {
+            for (String partitionKey : partitionSpec.keySet()) {
+                if (!partitionKeys.contains(partitionKey)) {
+                    invalidKeys.add(partitionKey);
+                }
+            }
+        }
+
+        checkArgument(
+                invalidKeys.isEmpty(),
+                "Partition keys %s are invalid. Available partition keys are 
%s",
+                invalidKeys,
+                partitionKeys);
+    }
+
+    private static List<Map<String, String>> 
normalizePartitionValuesWithSparkParser(
+            SparkSession spark, List<Map<String, String>> partitionSpecs) {
+        return partitionSpecs.stream()
+                .map(partitionSpec -> parsePartitionSpec(spark, partitionSpec, 
false))
+                .collect(java.util.stream.Collectors.toList());
+    }
+
+    /**
+     * Parse static partition spec values by evaluating them as Spark SQL 
literal expressions. This
+     * keeps overwrite partition handling aligned with Spark SQL partition 
specs, while rejecting
+     * non-literal or null values.
+     *
+     * @param spark the Spark session
+     * @param partitionSpec the partition spec with raw values (e.g., {"date": 
"\"20260225\""})
+     * @return the static partition map with parsed literal values (e.g., 
{"date": "20260225"})
+     */
+    public static Map<String, String> parseStaticPartition(
+            SparkSession spark, Map<String, String> partitionSpec) {
+        return parsePartitionSpec(spark, partitionSpec, true);
+    }
+
+    private static Map<String, String> parsePartitionSpec(
+            SparkSession spark, Map<String, String> partitionSpec, boolean 
requireLiteral) {
+        Map<String, String> staticPartition = new LinkedHashMap<>();
+        for (Map.Entry<String, String> entry : partitionSpec.entrySet()) {
+            staticPartition.put(
+                    entry.getKey(), parsePartitionValue(spark, 
entry.getValue(), requireLiteral));
+        }
+        return staticPartition;
+    }
+
+    private static String parsePartitionValue(
+            SparkSession spark, String value, boolean requireLiteral) {
+        Expression expr;
+        try {
+            expr = spark.sessionState().sqlParser().parseExpression(value);
+        } catch (Exception e) {
+            if (requireLiteral) {
+                throw new RuntimeException(e);
+            }
+            return value;
+        }
+        if (!(expr instanceof Literal)) {
+            checkArgument(
+                    !requireLiteral,
+                    "Partition value must be a literal expression, but got: 
%s",
+                    value);
+            return value;
+        }
+
+        Object literalValue = ((Literal) expr).value();
+        if (literalValue == null) {
+            checkArgument(!requireLiteral, "Partition value cannot be null");
+            return value;
+        }
+        return literalValue.toString();
+    }
+
     public static int readParallelism(List<?> groupedTasks, SparkSession 
spark) {
         int sparkParallelism =
                 Math.max(
@@ -109,33 +221,4 @@ public class SparkProcedureUtils {
                 .reduce((a, b) -> a + " OR " + b)
                 .orElse(null);
     }
-
-    /**
-     * Parse partition spec values by evaluating them as Spark SQL literal 
expressions. This strips
-     * quotes from string literals and validates that values are non-null 
literals.
-     *
-     * @param spark the Spark session
-     * @param partitionSpec the partition spec with raw values (e.g., {"date": 
"\"20260225\""})
-     * @return the static partition map with unquoted literal values (e.g., 
{"date": "20260225"})
-     */
-    public static Map<String, String> parseStaticPartition(
-            SparkSession spark, Map<String, String> partitionSpec) {
-        Map<String, String> staticPartition = new HashMap<>();
-        for (Map.Entry<String, String> entry : partitionSpec.entrySet()) {
-            Expression expr;
-            try {
-                expr = 
spark.sessionState().sqlParser().parseExpression(entry.getValue());
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
-            checkArgument(
-                    expr instanceof Literal,
-                    "Partition value must be a literal expression, but got: 
%s",
-                    entry.getValue());
-            Object value = ((Literal) expr).value();
-            checkArgument(value != null, "Partition value cannot be null");
-            staticPartition.put(entry.getKey(), value.toString());
-        }
-        return staticPartition;
-    }
 }
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 7cbe2b6659..76218f19ef 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
@@ -30,6 +30,7 @@ import org.apache.spark.sql.{Dataset, Row}
 import org.apache.spark.sql.paimon.shims.memstream.MemoryStream
 import org.apache.spark.sql.streaming.StreamTest
 import org.assertj.core.api.Assertions
+import org.assertj.core.api.Assertions.assertThatThrownBy
 import org.scalatest.time.Span
 
 import java.util
@@ -537,7 +538,7 @@ abstract class CompactProcedureTestBase extends 
PaimonSparkTestBase with StreamT
 
     assert(intercept[IllegalArgumentException] {
       spark.sql("CALL sys.compact(table => 'T', partitions => 'id = 1')")
-    }.getMessage.contains("Only partition predicate is supported"))
+    }.getMessage.contains("Partition keys [id] are invalid"))
 
     assert(intercept[IllegalArgumentException] {
       spark.sql("CALL sys.compact(table => 'T', where => 'id > 1 AND pt = 
\"p1\"')")
@@ -1533,4 +1534,42 @@ abstract class CompactProcedureTestBase extends 
PaimonSparkTestBase with StreamT
   def lastSnapshotId(table: FileStoreTable): Long = {
     table.snapshotManager().latestSnapshotId()
   }
+
+  test("Paimon Procedure: compact partitions accept unquoted string values") {
+    withTable("T") {
+      spark.sql(s"""
+                   |CREATE TABLE T (id INT, value STRING, dt STRING, hh INT)
+                   |TBLPROPERTIES ('bucket'='-1', 'write-only'='true')
+                   |PARTITIONED BY (dt, hh)
+                   |""".stripMargin)
+
+      val table = loadTable("T")
+
+      spark.sql(s"INSERT INTO T VALUES (1, 'a', '2024-01-01', 0), (2, 'b', 
'2024-01-01', 0)")
+      spark.sql(s"INSERT INTO T VALUES (3, 'c', '2024-01-02', 0), (4, 'd', 
'2024-01-02', 0)")
+
+      val before = lastSnapshotId(table)
+      checkAnswer(
+        spark.sql(
+          "CALL sys.compact(table => 'T', partitions => 'dt=2024-01-01,hh=0', 
options => 'compaction.min.file-num=2')"),
+        Row(true) :: Nil)
+
+      
Assertions.assertThat(lastSnapshotId(loadTable("T"))).isGreaterThan(before)
+    }
+  }
+
+  test("Paimon Procedure: compact with invalid partition key") {
+    withTable("T") {
+      spark.sql(s"""
+                   |CREATE TABLE T (id INT, value STRING, dt STRING, hh INT)
+                   |TBLPROPERTIES ('bucket'='-1', 'write-only'='true')
+                   |PARTITIONED BY (dt, hh)
+                   |""".stripMargin)
+
+      val e = intercept[IllegalArgumentException] {
+        spark.sql("CALL sys.compact(table => 'T', partitions => 
'pt=2024-01-01')")
+      }
+      Assertions.assertThat(e.getMessage.contains("Partition keys [pt] are 
invalid"))
+    }
+  }
 }
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/RescaleProcedureTest.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/RescaleProcedureTest.scala
index 986b1f9b6b..b4a6525116 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/RescaleProcedureTest.scala
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/procedure/RescaleProcedureTest.scala
@@ -146,6 +146,37 @@ class RescaleProcedureTest extends PaimonSparkTestBase {
     }
   }
 
+  test("Paimon Procedure: rescale partitions accept unquoted string values") {
+    withTable("T") {
+      spark.sql(s"""
+                   |CREATE TABLE T (id INT, value STRING, dt STRING, hh INT)
+                   |TBLPROPERTIES ('primary-key'='id, dt, hh', 'bucket'='2')
+                   |PARTITIONED BY (dt, hh)
+                   |""".stripMargin)
+
+      spark.sql(s"INSERT INTO T VALUES (1, 'a', '2024-01-01', 0), (2, 'b', 
'2024-01-01', 0)")
+      spark.sql(s"INSERT INTO T VALUES (3, 'c', '2024-01-02', 0), (4, 'd', 
'2024-01-02', 0)")
+
+      spark.sql("ALTER TABLE T SET TBLPROPERTIES ('bucket' = '4')")
+      checkAnswer(
+        spark.sql(
+          "CALL sys.rescale(table => 'T', bucket_num => 4, partitions => 
'dt=2024-01-01,hh=0')"),
+        Row(true) :: Nil)
+
+      val reloadedTable = loadTable("T")
+      val predicate = PartitionPredicate.fromMap(
+        reloadedTable.schema().logicalPartitionType(),
+        Map("dt" -> "2024-01-01", "hh" -> "0").asJava,
+        reloadedTable.coreOptions().partitionDefaultName())
+      reloadedTable.newSnapshotReader
+        .withPartitionFilter(predicate)
+        .read
+        .dataSplits
+        .asScala
+        .foreach(split => Assertions.assertThat(split.bucket()).isLessThan(4))
+    }
+  }
+
   test("Paimon Procedure: rescale with where clause") {
     withTable("T") {
       spark.sql(s"""

Reply via email to