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 f87bf749df [spark] Make max_pt work on format tables (#9024)
f87bf749df is described below

commit f87bf749df776642b4ce113bb3de65c58a9301d2
Author: Dapeng Sun(孙大鹏) <[email protected]>
AuthorDate: Tue Aug 4 18:52:45 2026 +0800

    [spark] Make max_pt work on format tables (#9024)
---
 .../catalyst/analysis/ReplacePaimonFunctions.scala | 65 ++++++++++++---
 .../paimon/spark/table/PaimonFormatTableTest.scala | 97 ++++++++++++++++++++++
 2 files changed, 150 insertions(+), 12 deletions(-)

diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/analysis/ReplacePaimonFunctions.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/analysis/ReplacePaimonFunctions.scala
index 5a11384246..d81560ffe6 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/analysis/ReplacePaimonFunctions.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/catalyst/analysis/ReplacePaimonFunctions.scala
@@ -18,12 +18,15 @@
 
 package org.apache.paimon.spark.catalyst.analysis
 
-import org.apache.paimon.spark.{DataConverter, SparkTable, SparkTypeUtils, 
SparkUtils}
+import org.apache.paimon.partition.PartitionPredicate
+import org.apache.paimon.predicate.PredicateBuilder
+import org.apache.paimon.spark.{BaseTable, DataConverter, SparkTable, 
SparkTypeUtils, SparkUtils}
 import org.apache.paimon.spark.catalog.SparkBaseCatalog
 import org.apache.paimon.spark.catalog.functions.PaimonFunctions
 import org.apache.paimon.spark.function.{BlobViewFieldIdSparkFunction, 
BlobViewSparkFunction, DescriptorToPresignedUrlFunction, 
ResolvedDescriptorToPresignedUrlFunction}
 import org.apache.paimon.spark.utils.CatalogUtils
 import org.apache.paimon.table.DataTable
+import org.apache.paimon.table.FormatTable
 import org.apache.paimon.types.DataTypeRoot
 import org.apache.paimon.utils.{InternalRowUtils, TypeUtils}
 
@@ -151,21 +154,59 @@ case class ReplacePaimonFunctions(spark: SparkSession) 
extends Rule[LogicalPlan]
 
     val table =
       
catalogAndIdentifier.catalog.asTableCatalog.loadTable(catalogAndIdentifier.identifier())
-    assert(table.isInstanceOf[SparkTable])
-    val sparkTable = table.asInstanceOf[SparkTable]
-    if (sparkTable.table.partitionKeys().size() == 0) {
+    // Every Paimon table wrapper exposes the underlying table through 
BaseTable. Asserting the
+    // narrower SparkTable used to fail a format table with an AssertionError 
rather than telling
+    // the user anything.
+    if (!table.isInstanceOf[BaseTable]) {
+      throw new UnsupportedOperationException(s"$table is not a Paimon table")
+    }
+    val paimonTable = table.asInstanceOf[BaseTable].table
+    if (paimonTable.partitionKeys().size() == 0) {
       throw new UnsupportedOperationException(s"$table is not a partitioned 
table")
     }
 
     val toplevelPartitionType =
-      TypeUtils.project(sparkTable.table.rowType, 
sparkTable.table.partitionKeys()).getTypeAt(0)
-    val partitionValues = sparkTable.table.newReadBuilder.newScan
-      .listPartitionEntries()
-      .asScala
-      .filter(_.fileCount() > 0)
-      .map {
-        partitionEntry => InternalRowUtils.get(partitionEntry.partition(), 0, 
toplevelPartitionType)
-      }
+      TypeUtils.project(paimonTable.rowType, 
paimonTable.partitionKeys()).getTypeAt(0)
+    val partitions = paimonTable match {
+      case formatTable: FormatTable =>
+        val partitionType = TypeUtils.project(paimonTable.rowType, 
paimonTable.partitionKeys())
+        val candidates = formatTable.newReadBuilder.newScan
+          .listPartitionEntries()
+          .asScala
+          .map(entry => InternalRowUtils.get(entry.partition(), 0, 
toplevelPartitionType))
+          .filter(_ != null)
+          .distinct
+          .sortWith(InternalRowUtils.compare(_, _, 
toplevelPartitionType.getTypeRoot) > 0)
+
+        // Catalog metadata uses zero both for an empty format table partition 
and for a
+        // partition whose file count has not been reported. Check candidates 
from largest to
+        // smallest and stop at the first value whose filtered scan produces 
data. This also avoids
+        // treating an empty filesystem directory as data without planning 
splits for the whole
+        // table.
+        candidates.find {
+          candidate =>
+            val predicate = new PredicateBuilder(partitionType).equal(0, 
candidate)
+            val partitionFilter =
+              PartitionPredicate.fromPredicate(partitionType, predicate)
+            !formatTable.newReadBuilder
+              .withPartitionFilter(partitionFilter)
+              .newScan
+              .plan()
+              .splits()
+              .isEmpty
+        }.toSeq
+      case _ =>
+        // FileStoreTable manifests carry an exact file count, so keep the 
cheaper metadata path.
+        paimonTable.newReadBuilder.newScan
+          .listPartitionEntries()
+          .asScala
+          .filter(_.fileCount() > 0)
+          .map(entry => InternalRowUtils.get(entry.partition(), 0, 
toplevelPartitionType))
+    }
+    val partitionValues = partitions
+      // The default partition comes back as a real null, which 
InternalRowUtils.compare would
+      // dereference. It is also not a value anyone means by "the max 
partition".
+      .filter(_ != null)
       .sortWith(InternalRowUtils.compare(_, _, 
toplevelPartitionType.getTypeRoot) < 0)
       .map(DataConverter.fromPaimon(_, toplevelPartitionType))
     if (partitionValues.isEmpty) {
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonFormatTableTest.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonFormatTableTest.scala
index adac70df2e..fedf8363d0 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonFormatTableTest.scala
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/table/PaimonFormatTableTest.scala
@@ -245,6 +245,103 @@ class PaimonFormatTableTest extends 
PaimonSparkTestWithRestCatalogBase {
     }
   }
 
+  test("PaimonFormatTable: max_pt picks the largest partition") {
+    val tableName = "max_pt_t"
+    withTable(tableName) {
+      sql(
+        s"CREATE TABLE $tableName (f0 INT) USING CSV PARTITIONED BY (ds 
STRING) " +
+          "TBLPROPERTIES ('format-table.implementation'='paimon', " +
+          "'metastore.partitioned-table'='true')")
+      val table =
+        paimonCatalog.getTable(Identifier.create("test_db", 
tableName)).asInstanceOf[FormatTable]
+      assert(table.partitionManager() != null)
+      sql(s"INSERT INTO $tableName VALUES (1, '20240101')")
+      sql(s"INSERT INTO $tableName VALUES (2, '20240103')")
+      sql(s"INSERT INTO $tableName VALUES (3, '20240102')")
+
+      // Two things used to stop max_pt on a format table: it is not a 
SparkTable, and
+      // catalog partitions report zero before the partition-statistics 
contract exists.
+      checkAnswer(sql(s"SELECT sys.max_pt('test_db.$tableName')"), 
Seq(Row("20240103")))
+      checkAnswer(
+        sql(s"SELECT * FROM $tableName WHERE ds = 
sys.max_pt('test_db.$tableName')"),
+        Seq(Row(2, "20240103")))
+    }
+  }
+
+  test("PaimonFormatTable: max_pt skips the default partition") {
+    val tableName = "max_pt_null"
+    withTable(tableName) {
+      sql(
+        s"CREATE TABLE $tableName (f0 INT) USING CSV PARTITIONED BY (ds 
STRING) " +
+          "TBLPROPERTIES ('format-table.implementation'='paimon', " +
+          "'metastore.partitioned-table'='true')")
+      sql(s"INSERT INTO $tableName VALUES (1, '20240101')")
+      // A null partition value comes back as a real null, which the typed 
comparator would
+      // dereference; it is also not what anyone means by the max partition.
+      sql(s"INSERT INTO $tableName VALUES (2, null)")
+
+      checkAnswer(sql(s"SELECT sys.max_pt('test_db.$tableName')"), 
Seq(Row("20240101")))
+    }
+  }
+
+  test("PaimonFormatTable: max_pt skips an empty registered partition") {
+    val tableName = "max_pt_empty"
+    withTable(tableName) {
+      sql(
+        s"CREATE TABLE $tableName (f0 INT) USING CSV PARTITIONED BY (ds 
STRING) " +
+          "TBLPROPERTIES ('format-table.implementation'='paimon', " +
+          "'metastore.partitioned-table'='true')")
+      sql(s"INSERT INTO $tableName VALUES (1, '20240101')")
+      sql(s"ALTER TABLE $tableName ADD PARTITION (ds='20240103')")
+
+      checkAnswer(sql(s"SELECT sys.max_pt('test_db.$tableName')"), 
Seq(Row("20240101")))
+    }
+  }
+
+  test("PaimonFormatTable: max_pt skips an empty filesystem partition 
directory") {
+    val tableName = "max_pt_empty_dir"
+    withTable(tableName) {
+      sql(
+        s"CREATE TABLE $tableName (f0 INT) USING CSV PARTITIONED BY (ds 
STRING) " +
+          "TBLPROPERTIES ('format-table.implementation'='paimon')")
+      sql(s"INSERT INTO $tableName VALUES (1, '20240101')")
+      val table =
+        paimonCatalog.getTable(Identifier.create("test_db", 
tableName)).asInstanceOf[FormatTable]
+      assert(table.partitionManager() == null)
+      table.fileIO().mkdirs(new Path(table.location(), "ds=20240103"))
+
+      checkAnswer(sql(s"SELECT sys.max_pt('test_db.$tableName')"), 
Seq(Row("20240101")))
+    }
+  }
+
+  test("max_pt on a FileStoreTable skips a null partition instead of failing") 
{
+    val tableName = "max_pt_fst_null"
+    withTable(tableName) {
+      sql(s"CREATE TABLE $tableName (id INT, ds STRING) USING paimon 
PARTITIONED BY (ds)")
+      sql(s"INSERT INTO $tableName VALUES (1, '20240101')")
+      sql(s"INSERT INTO $tableName VALUES (2, null)")
+
+      // This is a behaviour change for FileStoreTable, not only for format 
tables: a null
+      // top-level partition value used to reach InternalRowUtils.compare and 
throw, and is now
+      // skipped. Pinned here so the change is visible rather than incidental.
+      checkAnswer(sql(s"SELECT sys.max_pt('test_db.$tableName')"), 
Seq(Row("20240101")))
+    }
+  }
+
+  test("PaimonFormatTable: max_pt rejects an unpartitioned table") {
+    val tableName = "max_pt_flat"
+    withTable(tableName) {
+      sql(
+        s"CREATE TABLE $tableName (f0 INT) USING CSV " +
+          "TBLPROPERTIES ('format-table.implementation'='paimon')")
+      sql(s"INSERT INTO $tableName VALUES (1)")
+      val e = intercept[Exception] {
+        sql(s"SELECT sys.max_pt('test_db.$tableName')").collect()
+      }
+      assert(e.getMessage.contains("not a partitioned table"))
+    }
+  }
+
   test("PaimonFormatTable: non-partitioned table") {
     for {
       (format, compression) <- Seq(

Reply via email to