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

YannByron 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 b04db5a86a [spark] Support deleted record metrics for V2 delete (#8486)
b04db5a86a is described below

commit b04db5a86a82edc6f8d7e0cd61cc25ea07aa7b2b
Author: Kerwin Zhang <[email protected]>
AuthorDate: Fri Jul 10 10:35:34 2026 +0800

    [spark] Support deleted record metrics for V2 delete (#8486)
---
 .../org/apache/paimon/spark/PaimonMetrics.scala    | 10 +++++++
 .../paimon/spark/write/PaimonBatchWriteBase.scala  | 34 +++++++++++++++++++---
 .../apache/paimon/spark/write/PaimonV2Write.scala  |  7 ++++-
 .../apache/paimon/spark/sql/PaimonMetricTest.scala |  2 ++
 4 files changed, 48 insertions(+), 5 deletions(-)

diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonMetrics.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonMetrics.scala
index c8ceba357b..d3d2a39364 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonMetrics.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonMetrics.scala
@@ -40,6 +40,7 @@ object PaimonMetrics {
   val ADDED_TABLE_FILES = "addedTableFiles"
   val DELETED_TABLE_FILES = "deletedTableFiles"
   val INSERTED_RECORDS = "insertedRecords"
+  val DELETED_RECORDS = "deletedRecords"
   val APPENDED_CHANGELOG_FILES = "appendedChangelogFiles"
   val PARTITIONS_WRITTEN = "partitionsWritten"
   val BUCKETS_WRITTEN = "bucketsWritten"
@@ -211,6 +212,15 @@ case class PaimonInsertedRecordsTaskMetric(value: Long) 
extends PaimonTaskMetric
   override def name(): String = PaimonMetrics.INSERTED_RECORDS
 }
 
+case class PaimonDeletedRecordsMetric() extends PaimonSumMetric {
+  override def name(): String = PaimonMetrics.DELETED_RECORDS
+  override def description(): String = "number of deleted records"
+}
+
+case class PaimonDeletedRecordsTaskMetric(value: Long) extends 
PaimonTaskMetric {
+  override def name(): String = PaimonMetrics.DELETED_RECORDS
+}
+
 case class PaimonAppendedChangelogFilesMetric() extends PaimonSumMetric {
   override def name(): String = PaimonMetrics.APPENDED_CHANGELOG_FILES
   override def description(): String = "number of appended changelog files"
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/PaimonBatchWriteBase.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/PaimonBatchWriteBase.scala
index e925cad76b..b13cea4b3b 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/PaimonBatchWriteBase.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/PaimonBatchWriteBase.scala
@@ -20,7 +20,7 @@ package org.apache.paimon.spark.write
 
 import org.apache.paimon.Snapshot
 import org.apache.paimon.io.{CompactIncrement, DataFileMeta, DataIncrement}
-import org.apache.paimon.spark.SparkTypeUtils
+import org.apache.paimon.spark.{PaimonDeletedRecordsTaskMetric, SparkTypeUtils}
 import org.apache.paimon.spark.catalyst.Compatibility
 import org.apache.paimon.spark.commands.SparkDataFileMeta
 import org.apache.paimon.spark.metric.SparkMetricRegistry
@@ -30,6 +30,7 @@ import org.apache.paimon.table.{FileStoreTable, SpecialFields}
 import org.apache.paimon.table.sink.{BatchWriteBuilder, CommitMessage, 
CommitMessageImpl}
 
 import org.apache.spark.sql.PaimonSparkSession
+import org.apache.spark.sql.connector.metric.CustomTaskMetric
 import org.apache.spark.sql.connector.write.{DataWriterFactory, 
PhysicalWriteInfo, WriterCommitMessage}
 import org.apache.spark.sql.execution.SQLExecution
 import org.apache.spark.sql.execution.metric.SQLMetrics
@@ -132,10 +133,35 @@ abstract class PaimonBatchWriteBase(
     } finally {
       batchTableCommit.close()
     }
-    postDriverMetrics()
+    postDriverMetrics(deletedRecordsTaskMetric(operation, addCommitMessage, 
deletedCommitMessage))
     postCommit(commitMessages)
   }
 
+  /**
+   * For copy-on-write DELETE, the number of deleted records is exactly the 
row count of the removed
+   * files minus the row count of the rewritten files. This does not hold for 
UPDATE and MERGE,
+   * whose rewritten files mix copied rows with modified rows.
+   */
+  private def deletedRecordsTaskMetric(
+      operation: Snapshot.Operation,
+      addedMessages: Seq[CommitMessage],
+      deletedMessages: Seq[CommitMessage]): Array[CustomTaskMetric] = {
+    if (copyOnWriteScan.isEmpty || operation != Snapshot.Operation.DELETE) {
+      return Array.empty
+    }
+    val addedRecords = addedMessages
+      .collect { case m: CommitMessageImpl => m }
+      .flatMap(_.newFilesIncrement().newFiles().asScala)
+      .map(_.rowCount())
+      .sum
+    val deletedRecords = deletedMessages
+      .collect { case m: CommitMessageImpl => m }
+      .flatMap(_.newFilesIncrement().deletedFiles().asScala)
+      .map(_.rowCount())
+      .sum
+    Array(PaimonDeletedRecordsTaskMetric(deletedRecords - addedRecords))
+  }
+
   protected def abortMessages(messages: Array[WriterCommitMessage]): Unit = {
     if (commitStarted) {
       logWarning(s"Skip abort cleanup for table ${table.name()} because commit 
has already started")
@@ -154,10 +180,10 @@ abstract class PaimonBatchWriteBase(
 
   // Spark support v2 write driver metrics since 4.0, see 
https://github.com/apache/spark/pull/48573
   // To ensure compatibility with 3.x, manually post driver metrics here 
instead of using Spark's API.
-  protected def postDriverMetrics(): Unit = {
+  protected def postDriverMetrics(extraMetrics: Array[CustomTaskMetric] = 
Array.empty): Unit = {
     val spark = PaimonSparkSession.active
     // todo: find a more suitable way to get metrics.
-    val commitMetrics = metricRegistry.buildSparkCommitMetrics()
+    val commitMetrics = metricRegistry.buildSparkCommitMetrics() ++ 
extraMetrics
     val executionId = 
spark.sparkContext.getLocalProperty(SQLExecution.EXECUTION_ID_KEY)
     val executionMetrics = Compatibility.getExecutionMetrics(spark, 
executionId.toLong).distinct
     val metricUpdates = executionMetrics.flatMap {
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/PaimonV2Write.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/PaimonV2Write.scala
index 6dd02172a6..a6b49a7918 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/PaimonV2Write.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/PaimonV2Write.scala
@@ -84,11 +84,16 @@ class PaimonV2Write(
       PaimonAddedTableFilesMetric()
     )
     if (copyOnWriteScan.isEmpty) {
-      // todo: support record metrics for row level ops
       buffer += PaimonInsertedRecordsMetric()
     }
     if (copyOnWriteScan.nonEmpty) {
       buffer += PaimonDeletedTableFilesMetric()
+      // For DELETE, the number of deleted records is exactly the row count 
difference between
+      // the removed files and the rewritten files. For UPDATE and MERGE, the 
rewritten files
+      // mix copied rows with modified rows, so record metrics cannot be 
derived from file stats.
+      if (operationType.contains(Snapshot.Operation.DELETE)) {
+        buffer += PaimonDeletedRecordsMetric()
+      }
     }
     if (!coreOptions.changelogProducer().equals(ChangelogProducer.NONE)) {
       buffer += PaimonAppendedChangelogFilesMetric()
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PaimonMetricTest.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PaimonMetricTest.scala
index c2acc7e928..d2e23d097d 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PaimonMetricTest.scala
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/PaimonMetricTest.scala
@@ -185,12 +185,14 @@ class PaimonMetricTest extends PaimonSparkTestBase with 
ScanPlanHelper {
       val executionMetrics = commandExecutionMetrics(metrics)
       assert(executionMetrics(metrics("addedTableFiles").id) == "1")
       assert(executionMetrics(metrics("deletedTableFiles").id) == "1")
+      assert(executionMetrics(metrics("deletedRecords").id) == "3")
 
       val df1 = sql("DELETE FROM T WHERE id > 0")
       val metrics1 = commandMetrics(df1)
       val executionMetrics1 = commandExecutionMetrics(metrics1)
       assert(executionMetrics1(metrics1("addedTableFiles").id) == "0")
       assert(executionMetrics1(metrics1("deletedTableFiles").id) == "1")
+      assert(executionMetrics1(metrics1("deletedRecords").id) == "7")
     }
   }
 

Reply via email to