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 87bdb2d5d7 [spark] Support action predicate pruning for data evolution 
self-merge (#9544)
87bdb2d5d7 is described below

commit 87bdb2d5d73ab288945752ec109b9ee52f99f2cd
Author: zhoulii <[email protected]>
AuthorDate: Wed Sep 2 17:52:54 2026 +0800

    [spark] Support action predicate pruning for data evolution self-merge 
(#9544)
---
 .../MergeIntoPaimonDataEvolutionTable.scala        | 104 ++++++++----
 .../MergeIntoPaimonDataEvolutionTable.scala        | 104 ++++++++----
 .../paimon/spark/sql/RowTrackingTestBase.scala     | 182 ++++++++++++++++++++-
 3 files changed, 325 insertions(+), 65 deletions(-)

diff --git 
a/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala
 
b/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala
index 1e59af4b57..d86165352d 100644
--- 
a/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala
+++ 
b/paimon-spark/paimon-spark-4.0/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala
@@ -28,6 +28,7 @@ import org.apache.paimon.index.GlobalIndexMeta
 import org.apache.paimon.io.{CompactIncrement, DataIncrement}
 import org.apache.paimon.manifest.IndexManifestEntry
 import org.apache.paimon.options.Options
+import org.apache.paimon.predicate.{Predicate, PredicateBuilder}
 import org.apache.paimon.spark.SparkTable
 import org.apache.paimon.spark.catalyst.analysis.PaimonRelation
 import org.apache.paimon.spark.catalyst.analysis.PaimonUpdateTable.toColumn
@@ -249,6 +250,32 @@ case class MergeIntoPaimonDataEvolutionTable(
 
   private lazy val targetRelation: DataSourceV2Relation = 
matchedUpdateScanTarget._2
 
+  private lazy val sourceToTargetAttributes: Map[ExprId, AttributeReference] = 
{
+    val targetAttrs = targetRelation.output ++ targetRelation.metadataOutput
+    val sourceAttrs = sourceTable.output ++ sourceTable.metadataOutput
+    sourceAttrs.flatMap {
+      source =>
+        targetAttrs.find(target => resolver(target.name, 
source.name)).map(source.exprId -> _)
+    }.toMap
+  }
+
+  private def rewriteSourceToTarget(expression: Expression): Expression = {
+    expression.transform {
+      case attr: AttributeReference if 
sourceToTargetAttributes.contains(attr.exprId) =>
+        sourceToTargetAttributes(attr.exprId)
+    }
+  }
+
+  private def rewriteToTargetOnly(expression: Expression): Option[Expression] 
= {
+    val rewritten = rewriteSourceToTarget(expression)
+    val targetExprIds = targetRelation.output.map(_.exprId).toSet
+    if (rewritten.references.forall(attr => 
targetExprIds.contains(attr.exprId))) {
+      Some(rewritten)
+    } else {
+      None
+    }
+  }
+
   lazy val tableSchema: StructType = targetSparkTable.schema
 
   override def run(sparkSession: SparkSession): Seq[Row] = {
@@ -264,7 +291,7 @@ case class MergeIntoPaimonDataEvolutionTable(
     if (readSnapshot != null) {
       snapshotReader.withSnapshot(readSnapshot)
     }
-    pushDownMergePartitionFilter(snapshotReader)
+    pushDownMergeFilters(snapshotReader)
     val plan = snapshotReader.read()
     val tableSplits: Seq[DataSplit] = plan
       .splits()
@@ -379,10 +406,17 @@ case class MergeIntoPaimonDataEvolutionTable(
     }
   }
 
-  private def pushDownMergePartitionFilter(snapshotReader: SnapshotReader): 
Unit = {
+  private def pushDownMergeFilters(snapshotReader: SnapshotReader): Unit = {
+    val predicates = Seq(mergePartitionPredicate, 
selfMergeActionPredicate).flatten
+    if (predicates.nonEmpty) {
+      snapshotReader.withFilter(PredicateBuilder.and(predicates: _*))
+    }
+  }
+
+  private def mergePartitionPredicate: Option[Predicate] = {
     val partitionRowType = table.schema().logicalPartitionType()
     if (partitionRowType.getFieldCount == 0) {
-      return
+      return None
     }
 
     // matchedCondition comes from MergeIntoTable.mergeCondition, which is the 
MERGE ON condition.
@@ -392,12 +426,42 @@ case class MergeIntoPaimonDataEvolutionTable(
       .getOrElse(Seq.empty)
 
     if (partitionPredicates.nonEmpty) {
-      val filter = convertConditionToPaimonPredicate(
+      convertConditionToPaimonPredicate(
         partitionPredicates.reduce(And),
         targetRelation.output,
         rowType,
         ignorePartialFailure = true)
-      filter.foreach(snapshotReader.withFilter)
+    } else {
+      None
+    }
+  }
+
+  private def selfMergeActionPredicate: Option[Predicate] = {
+    if (
+      !useSelfMergeShortcut ||
+      !table.coreOptions().dataEvolutionMergeIntoFilePruning() ||
+      matchedActions.isEmpty ||
+      matchedActions.exists(_.condition.isEmpty)
+    ) {
+      return None
+    }
+
+    val actionCondition = matchedActions.flatMap(_.condition).reduce(Or)
+    if (!actionCondition.deterministic) {
+      return None
+    }
+
+    rewriteToTargetOnly(actionCondition).flatMap {
+      targetOnlyCondition =>
+        try {
+          convertConditionToPaimonPredicate(
+            targetOnlyCondition,
+            targetRelation.output,
+            rowType,
+            ignorePartialFailure = false)
+        } catch {
+          case NonFatal(_) => None
+        }
     }
   }
 
@@ -433,8 +497,8 @@ case class MergeIntoPaimonDataEvolutionTable(
       firstRowIdToBlobFirstRowIds: Map[Long, List[Long]],
       persistSourceDss: Option[Dataset[Row]]): Seq[DataSplit] = {
     // Self-Merge shortcut:
-    // The snapshot-level partition filter has already removed unrelated 
partitions. Every row in
-    // the remaining splits may be updated, so the shortcut scans all of them.
+    // Snapshot-level merge filters have already removed unrelated splits. 
MergeRows preserves
+    // rows in the remaining splits which do not satisfy any matched action 
condition.
     if (useSelfMergeShortcut) {
       return tableSplits
     }
@@ -689,32 +753,14 @@ case class MergeIntoPaimonDataEvolutionTable(
         targetAttrsDedup.filter(a => neededNames.exists(n => resolver(n, 
a.name)))
       val readPlan = touchedFileTargetRelation.copy(output = 
allReadFieldsOnTarget)
 
-      // Build mapping: source exprId -> target attr (matched by column name).
-      val sourceToTarget = {
-        val targetAttrs = targetRelation.output ++ 
targetRelation.metadataOutput
-        val sourceAttrs = sourceTable.output ++ sourceTable.metadataOutput
-        sourceAttrs.flatMap {
-          s => targetAttrs.find(t => resolver(t.name, s.name)).map(t => 
s.exprId -> t)
-        }.toMap
-      }
-
-      def rewriteSourceToTarget(
-          expr: Expression,
-          m: Map[ExprId, AttributeReference]): Expression = {
-        expr.transform {
-          case a: AttributeReference if m.contains(a.exprId) => m(a.exprId)
-        }
-      }
-
       val rewrittenMatchedActions: Seq[MergeAction] = targetMatchedActions.map 
{
         case action: UpdateAction =>
-          val newCond = action.condition.map(c => rewriteSourceToTarget(c, 
sourceToTarget))
-          val newAssignments = action.assignments.map {
-            a => Assignment(a.key, rewriteSourceToTarget(a.value, 
sourceToTarget))
-          }
+          val newCond = action.condition.map(rewriteSourceToTarget)
+          val newAssignments =
+            action.assignments.map(a => Assignment(a.key, 
rewriteSourceToTarget(a.value)))
           action.copy(condition = newCond, assignments = newAssignments)
         case DeleteAction(condition) =>
-          DeleteAction(condition.map(c => rewriteSourceToTarget(c, 
sourceToTarget)))
+          DeleteAction(condition.map(rewriteSourceToTarget))
         case other =>
           throw new UnsupportedOperationException(s"Unsupported matched 
action: $other.")
       }
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala
index 9d24d1fcf8..b6a5650d1f 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala
@@ -28,6 +28,7 @@ import org.apache.paimon.index.GlobalIndexMeta
 import org.apache.paimon.io.{CompactIncrement, DataIncrement}
 import org.apache.paimon.manifest.IndexManifestEntry
 import org.apache.paimon.options.Options
+import org.apache.paimon.predicate.{Predicate, PredicateBuilder}
 import org.apache.paimon.spark.SparkTable
 import org.apache.paimon.spark.catalyst.analysis.PaimonRelation
 import org.apache.paimon.spark.catalyst.analysis.PaimonUpdateTable.toColumn
@@ -249,6 +250,32 @@ case class MergeIntoPaimonDataEvolutionTable(
 
   private lazy val targetRelation: DataSourceV2Relation = 
matchedUpdateScanTarget._2
 
+  private lazy val sourceToTargetAttributes: Map[ExprId, AttributeReference] = 
{
+    val targetAttrs = targetRelation.output ++ targetRelation.metadataOutput
+    val sourceAttrs = sourceTable.output ++ sourceTable.metadataOutput
+    sourceAttrs.flatMap {
+      source =>
+        targetAttrs.find(target => resolver(target.name, 
source.name)).map(source.exprId -> _)
+    }.toMap
+  }
+
+  private def rewriteSourceToTarget(expression: Expression): Expression = {
+    expression.transform {
+      case attr: AttributeReference if 
sourceToTargetAttributes.contains(attr.exprId) =>
+        sourceToTargetAttributes(attr.exprId)
+    }
+  }
+
+  private def rewriteToTargetOnly(expression: Expression): Option[Expression] 
= {
+    val rewritten = rewriteSourceToTarget(expression)
+    val targetExprIds = targetRelation.output.map(_.exprId).toSet
+    if (rewritten.references.forall(attr => 
targetExprIds.contains(attr.exprId))) {
+      Some(rewritten)
+    } else {
+      None
+    }
+  }
+
   lazy val tableSchema: StructType = targetSparkTable.schema
 
   override def run(sparkSession: SparkSession): Seq[Row] = {
@@ -264,7 +291,7 @@ case class MergeIntoPaimonDataEvolutionTable(
     if (readSnapshot != null) {
       snapshotReader.withSnapshot(readSnapshot)
     }
-    pushDownMergePartitionFilter(snapshotReader)
+    pushDownMergeFilters(snapshotReader)
     val plan = snapshotReader.read()
     val tableSplits: Seq[DataSplit] = plan
       .splits()
@@ -379,10 +406,17 @@ case class MergeIntoPaimonDataEvolutionTable(
     }
   }
 
-  private def pushDownMergePartitionFilter(snapshotReader: SnapshotReader): 
Unit = {
+  private def pushDownMergeFilters(snapshotReader: SnapshotReader): Unit = {
+    val predicates = Seq(mergePartitionPredicate, 
selfMergeActionPredicate).flatten
+    if (predicates.nonEmpty) {
+      snapshotReader.withFilter(PredicateBuilder.and(predicates: _*))
+    }
+  }
+
+  private def mergePartitionPredicate: Option[Predicate] = {
     val partitionRowType = table.schema().logicalPartitionType()
     if (partitionRowType.getFieldCount == 0) {
-      return
+      return None
     }
 
     // matchedCondition comes from MergeIntoTable.mergeCondition, which is the 
MERGE ON condition.
@@ -392,12 +426,42 @@ case class MergeIntoPaimonDataEvolutionTable(
       .getOrElse(Seq.empty)
 
     if (partitionPredicates.nonEmpty) {
-      val filter = convertConditionToPaimonPredicate(
+      convertConditionToPaimonPredicate(
         partitionPredicates.reduce(And),
         targetRelation.output,
         rowType,
         ignorePartialFailure = true)
-      filter.foreach(snapshotReader.withFilter)
+    } else {
+      None
+    }
+  }
+
+  private def selfMergeActionPredicate: Option[Predicate] = {
+    if (
+      !useSelfMergeShortcut ||
+      !table.coreOptions().dataEvolutionMergeIntoFilePruning() ||
+      matchedActions.isEmpty ||
+      matchedActions.exists(_.condition.isEmpty)
+    ) {
+      return None
+    }
+
+    val actionCondition = matchedActions.flatMap(_.condition).reduce(Or)
+    if (!actionCondition.deterministic) {
+      return None
+    }
+
+    rewriteToTargetOnly(actionCondition).flatMap {
+      targetOnlyCondition =>
+        try {
+          convertConditionToPaimonPredicate(
+            targetOnlyCondition,
+            targetRelation.output,
+            rowType,
+            ignorePartialFailure = false)
+        } catch {
+          case NonFatal(_) => None
+        }
     }
   }
 
@@ -433,8 +497,8 @@ case class MergeIntoPaimonDataEvolutionTable(
       firstRowIdToBlobFirstRowIds: Map[Long, List[Long]],
       persistSourceDss: Option[Dataset[Row]]): Seq[DataSplit] = {
     // Self-Merge shortcut:
-    // The snapshot-level partition filter has already removed unrelated 
partitions. Every row in
-    // the remaining splits may be updated, so the shortcut scans all of them.
+    // Snapshot-level merge filters have already removed unrelated splits. 
MergeRows preserves
+    // rows in the remaining splits which do not satisfy any matched action 
condition.
     if (useSelfMergeShortcut) {
       return tableSplits
     }
@@ -688,32 +752,14 @@ case class MergeIntoPaimonDataEvolutionTable(
         targetAttrsDedup.filter(a => neededNames.exists(n => resolver(n, 
a.name)))
       val readPlan = touchedFileTargetRelation.copy(output = 
allReadFieldsOnTarget)
 
-      // Build mapping: source exprId -> target attr (matched by column name).
-      val sourceToTarget = {
-        val targetAttrs = targetRelation.output ++ 
targetRelation.metadataOutput
-        val sourceAttrs = sourceTable.output ++ sourceTable.metadataOutput
-        sourceAttrs.flatMap {
-          s => targetAttrs.find(t => resolver(t.name, s.name)).map(t => 
s.exprId -> t)
-        }.toMap
-      }
-
-      def rewriteSourceToTarget(
-          expr: Expression,
-          m: Map[ExprId, AttributeReference]): Expression = {
-        expr.transform {
-          case a: AttributeReference if m.contains(a.exprId) => m(a.exprId)
-        }
-      }
-
       val rewrittenMatchedActions: Seq[MergeAction] = targetMatchedActions.map 
{
         case action: UpdateAction =>
-          val newCond = action.condition.map(c => rewriteSourceToTarget(c, 
sourceToTarget))
-          val newAssignments = action.assignments.map {
-            a => Assignment(a.key, rewriteSourceToTarget(a.value, 
sourceToTarget))
-          }
+          val newCond = action.condition.map(rewriteSourceToTarget)
+          val newAssignments =
+            action.assignments.map(a => Assignment(a.key, 
rewriteSourceToTarget(a.value)))
           action.copy(condition = newCond, assignments = newAssignments)
         case DeleteAction(condition) =>
-          DeleteAction(condition.map(c => rewriteSourceToTarget(c, 
sourceToTarget)))
+          DeleteAction(condition.map(rewriteSourceToTarget))
         case other =>
           throw new UnsupportedOperationException(s"Unsupported matched 
action: $other.")
       }
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/RowTrackingTestBase.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/RowTrackingTestBase.scala
index 5c4e32f390..6eb128e168 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/RowTrackingTestBase.scala
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/RowTrackingTestBase.scala
@@ -1087,9 +1087,11 @@ abstract class RowTrackingTestBase extends 
PaimonSparkTestBase with AdaptiveSpar
   test("Data Evolution: merge into table with data-evolution for Self-Merge 
with _ROW_ID shortcut") {
     withTable("target") {
       sql(
-        "CREATE TABLE target (a INT, b INT, c STRING) TBLPROPERTIES 
('row-tracking.enabled' = 'true', 'data-evolution.enabled' = 'true')")
+        "CREATE TABLE target (a INT, b INT, c STRING) TBLPROPERTIES 
('row-tracking.enabled' = 'true', 'data-evolution.enabled' = 'true', 
'compaction.min.file-num' = '100')")
       sql(
-        "INSERT INTO target values (1, 10, 'c1'), (2, 20, 'c2'), (3, 30, 
'c3'), (4, 40, 'c4'), (5, 50, 'c5')")
+        "INSERT INTO target SELECT /*+ REPARTITION(1) */ * FROM VALUES (1, 10, 
'c1'), (2, 20, 'c2')")
+      sql(
+        "INSERT INTO target SELECT /*+ REPARTITION(1) */ * FROM VALUES (3, 30, 
'c3'), (4, 40, 'c4'), (5, 50, 'c5')")
 
       var updatePlan: LogicalPlan = null
       val latch = new CountDownLatch(1)
@@ -1133,11 +1135,11 @@ abstract class RowTrackingTestBase extends 
PaimonSparkTestBase with AdaptiveSpar
       checkAnswer(
         sql("SELECT *, _ROW_ID, _SEQUENCE_NUMBER FROM target ORDER BY a"),
         Seq(
-          Row(1, 10, "c1", 0, 2),
-          Row(2, 20, "c2", 1, 2),
-          Row(3, 90, "c3c3", 2, 2),
-          Row(4, 120, "c4c4", 3, 2),
-          Row(5, 100, "c5", 4, 2))
+          Row(1, 10, "c1", 0, 1),
+          Row(2, 20, "c2", 1, 1),
+          Row(3, 90, "c3c3", 2, 3),
+          Row(4, 120, "c4c4", 3, 3),
+          Row(5, 100, "c5", 4, 3))
       )
     }
   }
@@ -1193,6 +1195,172 @@ abstract class RowTrackingTestBase extends 
PaimonSparkTestBase with AdaptiveSpar
     }
   }
 
+  Seq(false, true).foreach {
+    filePruning =>
+      test(s"Data Evolution: self-merge action predicate file pruning: 
$filePruning") {
+        withSparkSQLConf(
+          "spark.paimon.data-evolution.merge-into.file-pruning" -> 
filePruning.toString) {
+          withTable("target") {
+            sql("""
+                  |CREATE TABLE target (id INT, b INT) TBLPROPERTIES (
+                  |  'row-tracking.enabled' = 'true',
+                  |  'data-evolution.enabled' = 'true',
+                  |  'compaction.min.file-num' = '100')
+                  |""".stripMargin)
+            sql("INSERT INTO target SELECT /*+ REPARTITION(1) */ id, id * 10 
FROM range(1, 3)")
+            sql("INSERT INTO target SELECT /*+ REPARTITION(1) */ id, id * 10 
FROM range(3, 5)")
+
+            val (_, resultedTableFiles) =
+              executeMergeIntoAndCollectPlans("""
+                                                |MERGE INTO target
+                                                |USING target AS source
+                                                |ON target._ROW_ID = 
source._ROW_ID
+                                                |WHEN MATCHED AND source.id = 3
+                                                |  THEN UPDATE SET b = 
source.b + 100
+                                                |""".stripMargin)
+
+            val expectedFiles = if (filePruning) Seq(1L) else Seq(2L)
+            assert(
+              resultedTableFiles == expectedFiles,
+              s"Expected target scan metrics $expectedFiles, but got: " +
+                resultedTableFiles.mkString(", "))
+            checkAnswer(
+              sql("SELECT id, b FROM target ORDER BY id"),
+              Seq(Row(1, 10), Row(2, 20), Row(3, 130), Row(4, 40)))
+          }
+        }
+      }
+  }
+
+  test("Data Evolution: self-merge combines on and action predicates") {
+    withSparkSQLConf("spark.paimon.data-evolution.merge-into.file-pruning" -> 
"true") {
+      withTable("target") {
+        sql("""
+              |CREATE TABLE target (id INT, b INT, dt STRING) TBLPROPERTIES (
+              |  'row-tracking.enabled' = 'true',
+              |  'data-evolution.enabled' = 'true',
+              |  'compaction.min.file-num' = '100')
+              |PARTITIONED BY (dt)
+              |""".stripMargin)
+        sql("INSERT INTO target VALUES (1, 10, 'p1'), (2, 20, 'p1')")
+        sql("INSERT INTO target VALUES (1, 30, 'p2'), (4, 40, 'p2')")
+
+        val (_, resultedTableFiles) =
+          executeMergeIntoAndCollectPlans("""
+                                            |MERGE INTO target
+                                            |USING target AS source
+                                            |ON target._ROW_ID = source._ROW_ID
+                                            |  AND target.dt = 'p1'
+                                            |WHEN MATCHED AND source.id = 1
+                                            |  THEN UPDATE SET b = source.b + 
100
+                                            |""".stripMargin)
+
+        assert(
+          resultedTableFiles == Seq(1L),
+          s"Expected one target scan after combining predicates, but got: " +
+            resultedTableFiles.mkString(", "))
+        checkAnswer(
+          sql("SELECT id, b, dt FROM target ORDER BY dt, id"),
+          Seq(Row(1, 110, "p1"), Row(2, 20, "p1"), Row(1, 30, "p2"), Row(4, 
40, "p2")))
+      }
+    }
+  }
+
+  test("Data Evolution: self-merge skips action pruning for unconditional 
matched action") {
+    withSparkSQLConf("spark.paimon.data-evolution.merge-into.file-pruning" -> 
"true") {
+      withTable("target") {
+        sql("""
+              |CREATE TABLE target (id INT, b INT) TBLPROPERTIES (
+              |  'row-tracking.enabled' = 'true',
+              |  'data-evolution.enabled' = 'true',
+              |  'compaction.min.file-num' = '100')
+              |""".stripMargin)
+        sql("INSERT INTO target SELECT /*+ REPARTITION(1) */ id, id * 10 FROM 
range(1, 3)")
+        sql("INSERT INTO target SELECT /*+ REPARTITION(1) */ id, id * 10 FROM 
range(3, 5)")
+
+        val (_, resultedTableFiles) =
+          executeMergeIntoAndCollectPlans("""
+                                            |MERGE INTO target
+                                            |USING target AS source
+                                            |ON target._ROW_ID = source._ROW_ID
+                                            |WHEN MATCHED THEN UPDATE SET b = 
source.b + 100
+                                            |""".stripMargin)
+
+        assert(
+          resultedTableFiles == Seq(2L),
+          s"Expected one unpruned target scan, but got: 
${resultedTableFiles.mkString(", ")}")
+        checkAnswer(
+          sql("SELECT id, b FROM target ORDER BY id"),
+          Seq(Row(1, 110), Row(2, 120), Row(3, 130), Row(4, 140)))
+      }
+    }
+  }
+
+  test("Data Evolution: self-merge action predicate prunes files for delete") {
+    withSparkSQLConf("spark.paimon.data-evolution.merge-into.file-pruning" -> 
"true") {
+      withTable("target") {
+        sql("""
+              |CREATE TABLE target (id INT, b INT) TBLPROPERTIES (
+              |  'row-tracking.enabled' = 'true',
+              |  'data-evolution.enabled' = 'true',
+              |  'deletion-vectors.enabled' = 'true',
+              |  'compaction.min.file-num' = '100')
+              |""".stripMargin)
+        sql("INSERT INTO target SELECT /*+ REPARTITION(1) */ id, id * 10 FROM 
range(1, 3)")
+        sql("INSERT INTO target SELECT /*+ REPARTITION(1) */ id, id * 10 FROM 
range(3, 5)")
+
+        val (_, resultedTableFiles) =
+          executeMergeIntoAndCollectPlans("""
+                                            |MERGE INTO target
+                                            |USING target AS source
+                                            |ON target._ROW_ID = source._ROW_ID
+                                            |WHEN MATCHED AND source.id = 3 
THEN DELETE
+                                            |""".stripMargin)
+
+        assert(
+          resultedTableFiles == Seq(1L),
+          s"Expected one pruned target scan, but got: 
${resultedTableFiles.mkString(", ")}")
+        checkAnswer(
+          sql("SELECT id, b FROM target ORDER BY id"),
+          Seq(Row(1, 10), Row(2, 20), Row(4, 40)))
+      }
+    }
+  }
+
+  test("Data Evolution: self-merge action predicate requires complete 
conversion") {
+    withSparkSQLConf("spark.paimon.data-evolution.merge-into.file-pruning" -> 
"true") {
+      withTable("target") {
+        sql("""
+              |CREATE TABLE target (id INT, b INT) TBLPROPERTIES (
+              |  'row-tracking.enabled' = 'true',
+              |  'data-evolution.enabled' = 'true',
+              |  'compaction.min.file-num' = '100')
+              |""".stripMargin)
+        sql("INSERT INTO target SELECT /*+ REPARTITION(1) */ id, id * 10 FROM 
range(1, 3)")
+        sql("INSERT INTO target SELECT /*+ REPARTITION(1) */ id, id * 10 FROM 
range(3, 5)")
+
+        val (_, resultedTableFiles) =
+          executeMergeIntoAndCollectPlans("""
+                                            |MERGE INTO target
+                                            |USING target AS source
+                                            |ON target._ROW_ID = source._ROW_ID
+                                            |WHEN MATCHED AND target.id = 1
+                                            |  THEN UPDATE SET b = source.b + 
100
+                                            |WHEN MATCHED AND target.id + 1 = 4
+                                            |  THEN UPDATE SET b = source.b + 
200
+                                            |""".stripMargin)
+
+        assert(
+          resultedTableFiles == Seq(2L),
+          s"Expected action pruning to fall back completely, but got: " +
+            resultedTableFiles.mkString(", "))
+        checkAnswer(
+          sql("SELECT id, b FROM target ORDER BY id"),
+          Seq(Row(1, 110), Row(2, 20), Row(3, 230), Row(4, 40)))
+      }
+    }
+  }
+
   test("Data Evolution: self-merge falls back for computed source projection") 
{
     withTable("target") {
       sql("""

Reply via email to