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 d435ec9b05 [spark] adapt data evolution merge into for tables with 
existing deletion vectors (#8431)
d435ec9b05 is described below

commit d435ec9b050b84e7da769d148bb8d7218fd994dc
Author: Faiz <[email protected]>
AuthorDate: Thu Jul 2 23:34:34 2026 +0800

    [spark] adapt data evolution merge into for tables with existing deletion 
vectors (#8431)
    
    If call merge into on some data evolution tables with existing deletion
    vectors, there may exist some row id gaps of the merged output. Like:
    original table values:
    ```text
    (1, "a"), (2, "b"), (3, "c")
    ```
    After removing the second row, then merge into update with some source
    ```text
    (1, "new_a")
    ```
    The merged result would be
    ```text
    _ROW_ID, old_val, new_val
    
    (0, "a", "new_a")
    (2, "c", null)
    ```
    
    At that case, we should fill the gap of deleted value.
---
 .../spark/sql/DataEvolutionDeletionTest.scala      |  21 +++
 .../spark/sql/DataEvolutionDeletionTest.scala      |  21 +++
 .../spark/sql/DataEvolutionDeletionTest.scala      |  21 +++
 .../spark/sql/DataEvolutionDeletionTest.scala      |  33 ++++
 .../spark/sql/DataEvolutionDeletionTest.scala      |  33 ++++
 .../spark/sql/DataEvolutionDeletionTest.scala      |  33 ++++
 .../spark/write/DataEvolutionTableDataWrite.scala  |  54 +++++-
 .../spark/sql/DataEvolutionDeletionTestBase.scala  | 202 +++++++++++++++++++++
 .../paimon/spark/sql/DeleteFromTableTestBase.scala |  46 -----
 9 files changed, 416 insertions(+), 48 deletions(-)

diff --git 
a/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
 
b/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
new file mode 100644
index 0000000000..eb40890dae
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-3.2/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+class DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {}
diff --git 
a/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
 
b/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
new file mode 100644
index 0000000000..eb40890dae
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-3.3/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+class DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {}
diff --git 
a/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
 
b/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
new file mode 100644
index 0000000000..eb40890dae
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-3.4/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+class DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {}
diff --git 
a/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
 
b/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
new file mode 100644
index 0000000000..43aeb915db
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-3.5/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+import org.apache.spark.SparkConf
+
+class DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf.set("spark.paimon.write.use-v2-write", "false")
+  }
+}
+
+class V2DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf.set("spark.paimon.write.use-v2-write", "true")
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-4.0/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
 
b/paimon-spark/paimon-spark-4.0/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
new file mode 100644
index 0000000000..43aeb915db
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-4.0/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+import org.apache.spark.SparkConf
+
+class DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf.set("spark.paimon.write.use-v2-write", "false")
+  }
+}
+
+class V2DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf.set("spark.paimon.write.use-v2-write", "true")
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-4.1/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
 
b/paimon-spark/paimon-spark-4.1/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
new file mode 100644
index 0000000000..43aeb915db
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-4.1/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTest.scala
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+import org.apache.spark.SparkConf
+
+class DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf.set("spark.paimon.write.use-v2-write", "false")
+  }
+}
+
+class V2DataEvolutionDeletionTest extends DataEvolutionDeletionTestBase {
+  override protected def sparkConf: SparkConf = {
+    super.sparkConf.set("spark.paimon.write.use-v2-write", "true")
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/DataEvolutionTableDataWrite.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/DataEvolutionTableDataWrite.scala
index 50f2f691b0..b962f7ea32 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/DataEvolutionTableDataWrite.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/write/DataEvolutionTableDataWrite.scala
@@ -21,6 +21,7 @@ package org.apache.paimon.spark.write
 import org.apache.paimon.casting.FallbackMappingRow
 import org.apache.paimon.catalog.CatalogContext
 import org.apache.paimon.data.{BinaryRow, BlobPlaceholder, GenericRow, 
InternalRow}
+import org.apache.paimon.data.serializer.InternalSerializers
 import org.apache.paimon.disk.IOManager
 import org.apache.paimon.format.blob.BlobFileFormat.isBlobFile
 import org.apache.paimon.io.{CompactIncrement, DataIncrement}
@@ -34,6 +35,7 @@ import org.apache.paimon.utils.RecordWriter
 import org.apache.paimon.utils.SerializationUtils
 
 import org.apache.spark.sql.Row
+import org.slf4j.LoggerFactory
 
 import java.util.Collections
 
@@ -58,7 +60,9 @@ case class DataEvolutionTableDataWrite(
   private val toPaimonRow = {
     SparkRowUtils.toPaimonRow(writeType, -1, catalogContext)
   }
+  private lazy val rowSerializer = InternalSerializers.create(writeType)
   private val rawBlobFallbackFields = 
rawBlobPlaceholderMarkerIndexes.toSeq.sortBy(_._1).toArray
+  private val rawBlobFallbackFieldIndexes = rawBlobFallbackFields.map(_._1)
   private val rawBlobFallbackMappings = {
     val mappings = Array.fill(writeType.getFieldCount)(-1)
     rawBlobFallbackFields.zipWithIndex.foreach {
@@ -148,23 +152,43 @@ case class DataEvolutionTableDataWrite(
       recordWriter: RecordWriter[InternalRow],
       numRecords: Long) {
 
-    private var numWritten = 0
+    private var numWritten = 0L
+    private var fillerRows = 0L
+    private var fillerRow: InternalRow = _
 
     def matchFirstRowId(firstRowId: Long): Boolean = {
       this.firstRowId == firstRowId
     }
 
     def write(row: InternalRow, rowId: Long): Unit = {
-      assert(rowId == firstRowId + numWritten, "Row ID does not match 
expected.")
+      assert(
+        rowId >= firstRowId + numWritten,
+        s"Row ID should be incremental. Expected at least ${firstRowId + 
numWritten}, but got $rowId.")
+      assert(
+        rowId < firstRowId + numRecords,
+        s"Row ID $rowId is out of range [$firstRowId, ${firstRowId + 
numRecords}).")
+
+      // For tables with existing deletion vectors, there may be some row id 
gaps.
+      // We can simply pad any valid values since these rows will never be 
exposed.
+      fillGapUntil(rowId, row)
+
       numWritten += 1
       recordWriter.write(row)
     }
 
     def finish(): Seq[CommitMessageImpl] = {
       try {
+        fillGapUntil(firstRowId + numRecords)
+
         assert(
           numRecords == numWritten,
           s"Number of written records $numWritten does not match expected 
number $numRecords for first row ID $firstRowId.")
+        if (fillerRows > 0) {
+          DataEvolutionTableDataWrite.LOG.warn(
+            s"Data evolution merge wrote $fillerRows filler rows out of 
$numRecords rows " +
+              s"for row-id range [$firstRowId, ${firstRowId + numRecords}) to 
preserve " +
+              "row-id continuity. Raw blob fields in filler rows are written 
as NULL.")
+        }
         val result = recordWriter.prepareCommit(false)
         val dataFiles = result.newFilesIncrement().newFiles()
         val dataFileMetas = assignFirstRowIds(dataFiles.asScala.toSeq)
@@ -184,6 +208,28 @@ case class DataEvolutionTableDataWrite(
       }
     }
 
+    private def fillGapUntil(rowId: Long, fillerSourceRow: InternalRow = 
null): Unit = {
+      if (fillerRow == null && fillerSourceRow != null) {
+        // Copy the first record this file writer met to minimize the 
influences on
+        // file stats, but keep raw blob fields as NULLs so filler rows do not 
trigger
+        // blob fallback.
+        val copied = rowSerializer
+          .copyRowData(fillerSourceRow, new 
GenericRow(writeType.getFieldCount))
+          .asInstanceOf[GenericRow]
+        rawBlobFallbackFieldIndexes.foreach(copied.setField(_, null))
+        fillerRow = copied
+      }
+
+      assert(
+        fillerRow != null || firstRowId + numWritten == rowId,
+        s"Cannot fill row ID gaps before any real row for first row ID 
$firstRowId.")
+      while (firstRowId + numWritten < rowId) {
+        recordWriter.write(fillerRow)
+        numWritten += 1
+        fillerRows += 1
+      }
+    }
+
     private def assignFirstRowIds(dataFiles: 
Seq[org.apache.paimon.io.DataFileMeta])
         : Seq[org.apache.paimon.io.DataFileMeta] = {
       val assigned = ListBuffer[org.apache.paimon.io.DataFileMeta]()
@@ -223,3 +269,7 @@ case class DataEvolutionTableDataWrite(
     }
   }
 }
+
+object DataEvolutionTableDataWrite {
+  private val LOG = 
LoggerFactory.getLogger(classOf[DataEvolutionTableDataWrite])
+}
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTestBase.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTestBase.scala
new file mode 100644
index 0000000000..5d2ba1d697
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DataEvolutionDeletionTestBase.scala
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.sql
+
+import org.apache.paimon.spark.PaimonSparkTestBase
+
+import org.apache.spark.sql.Row
+
+abstract class DataEvolutionDeletionTestBase extends PaimonSparkTestBase {
+
+  test("Data Evolution deletion: delete from table with deletion vectors") {
+    withTable("t") {
+      sql("""
+            |CREATE TABLE t (id INT, b INT, c INT)
+            |TBLPROPERTIES (
+            |  'row-tracking.enabled' = 'true',
+            |  'data-evolution.enabled' = 'true',
+            |  'deletion-vectors.enabled' = 'true')
+            |""".stripMargin)
+      sql("INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c 
FROM range(0, 5)")
+      sql("INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c 
FROM range(5, 10)")
+      sql("ALTER TABLE t ADD COLUMNS (d INT)")
+      sql(
+        "INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c, id + 
100 AS d FROM range(10, 13)")
+
+      sql("DELETE FROM t WHERE id IN (1, 4, 6, 11)")
+      checkAnswer(
+        sql("SELECT *, _ROW_ID FROM t ORDER BY id"),
+        Seq(
+          Row(0, 0, 0, null, 0L),
+          Row(2, 2, 2, null, 2L),
+          Row(3, 3, 3, null, 3L),
+          Row(5, 5, 5, null, 5L),
+          Row(7, 7, 7, null, 7L),
+          Row(8, 8, 8, null, 8L),
+          Row(9, 9, 9, null, 9L),
+          Row(10, 10, 10, 110, 10L),
+          Row(12, 12, 12, 112, 12L)
+        )
+      )
+
+      sql("DELETE FROM t WHERE id IN (2, 8)")
+      checkAnswer(
+        sql("SELECT *, _ROW_ID FROM t ORDER BY id"),
+        Seq(
+          Row(0, 0, 0, null, 0L),
+          Row(3, 3, 3, null, 3L),
+          Row(5, 5, 5, null, 5L),
+          Row(7, 7, 7, null, 7L),
+          Row(9, 9, 9, null, 9L),
+          Row(10, 10, 10, 110, 10L),
+          Row(12, 12, 12, 112, 12L))
+      )
+    }
+  }
+
+  test("Data Evolution deletion: global index query skips deleted rows") {
+    withTable("t") {
+      sql("""
+            |CREATE TABLE t (id INT, name STRING, b INT)
+            |TBLPROPERTIES (
+            |  'row-tracking.enabled' = 'true',
+            |  'data-evolution.enabled' = 'true',
+            |  'deletion-vectors.enabled' = 'true',
+            |  'global-index.search-mode' = 'full',
+            |  'btree-index.records-per-range' = '1000')
+            |""".stripMargin)
+      sql("""
+            |INSERT INTO t VALUES
+            |  (1, 'name-1', 10),
+            |  (2, 'name-2', 20),
+            |  (3, 'name-3', 30),
+            |  (4, 'name-4', 40)
+            |""".stripMargin)
+      sql(
+        "CALL sys.create_global_index(table => 'test.t', index_column => 
'name', " +
+          "index_type => 'btree')")
+
+      sql("DELETE FROM t WHERE id IN (2, 4)")
+
+      checkAnswer(
+        sql("SELECT id, name, b FROM t WHERE name IN ('name-2', 'name-4') 
ORDER BY id"),
+        Seq.empty[Row])
+      checkAnswer(
+        sql("SELECT id, name, b FROM t WHERE name IN ('name-1', 'name-3') 
ORDER BY id"),
+        Seq(Row(1, "name-1", 10), Row(3, "name-3", 30)))
+    }
+  }
+
+  test("Data Evolution deletion: merge update after file-level and partial 
deletion") {
+    withTable("s", "t") {
+      sql("CREATE TABLE s (id INT, new_b INT)")
+      sql("INSERT INTO s VALUES (2, 200), (6, 600), (7, 700), (9, 900)")
+
+      sql("""
+            |CREATE TABLE t (id INT, b INT, c INT)
+            |TBLPROPERTIES (
+            |  'row-tracking.enabled' = 'true',
+            |  'data-evolution.enabled' = 'true',
+            |  'deletion-vectors.enabled' = 'true')
+            |""".stripMargin)
+      sql("INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c 
FROM range(0, 5)")
+      sql("INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c 
FROM range(5, 10)")
+      sql("DELETE FROM t WHERE id IN (0, 1, 2, 3, 4, 6, 9)")
+
+      sql("""
+            |MERGE INTO t
+            |USING s
+            |ON t.id = s.id
+            |WHEN MATCHED THEN UPDATE SET t.b = s.new_b
+            |""".stripMargin)
+
+      checkAnswer(
+        sql("SELECT id, b, c, _ROW_ID FROM t ORDER BY id"),
+        Seq(Row(5, 5, 5, 5L), Row(7, 700, 7, 7L), Row(8, 8, 8, 8L)))
+    }
+  }
+
+  test("Data Evolution deletion: merge update blob after deletion") {
+    withTable("s", "t") {
+      sql("""
+            |CREATE TABLE t (id INT, b INT, picture BINARY)
+            |TBLPROPERTIES (
+            |  'row-tracking.enabled' = 'true',
+            |  'data-evolution.enabled' = 'true',
+            |  'deletion-vectors.enabled' = 'true',
+            |  'blob-field' = 'picture',
+            |  'blob.target-file-size' = '1 b')
+            |""".stripMargin)
+      sql("""
+            |INSERT INTO t SELECT /*+ REPARTITION(1) */ id, b, picture FROM 
VALUES
+            |  (0, 0, X'00'), (1, 1, X'01'), (2, 2, X'02'), (3, 3, X'03'), (4, 
4, X'04')
+            |  AS v(id, b, picture)
+            |""".stripMargin)
+      sql("""
+            |INSERT INTO t SELECT /*+ REPARTITION(1) */ id, b, picture FROM 
VALUES
+            |  (5, 5, X'05'), (6, 6, X'06'), (7, 7, X'07'), (8, 8, X'08'), (9, 
9, X'09')
+            |  AS v(id, b, picture)
+            |""".stripMargin)
+      sql("DELETE FROM t WHERE id IN (0, 1, 2, 3, 4, 6, 9)")
+
+      sql("CREATE TABLE s (id INT, picture BINARY)")
+      sql("INSERT INTO s VALUES (2, X'22'), (6, X'66'), (7, X'4D'), (9, 
X'79')")
+
+      sql("""
+            |MERGE INTO t
+            |USING s
+            |ON t.id = s.id
+            |WHEN MATCHED THEN UPDATE SET t.picture = s.picture
+            |""".stripMargin)
+
+      checkAnswer(
+        sql("SELECT id, b, picture, _ROW_ID FROM t ORDER BY id"),
+        Seq(
+          Row(5, 5, Array[Byte](5), 5L),
+          Row(7, 7, Array[Byte](77), 7L),
+          Row(8, 8, Array[Byte](8), 8L)))
+    }
+  }
+
+  test("Data Evolution deletion: self merge skips deleted rows") {
+    withTable("t") {
+      sql("""
+            |CREATE TABLE t (id INT, b INT, c INT)
+            |TBLPROPERTIES (
+            |  'row-tracking.enabled' = 'true',
+            |  'data-evolution.enabled' = 'true',
+            |  'deletion-vectors.enabled' = 'true')
+            |""".stripMargin)
+      sql("INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c 
FROM range(0, 5)")
+      sql("INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c 
FROM range(5, 10)")
+      sql("DELETE FROM t WHERE id IN (0, 1, 2, 3, 4, 6, 9)")
+
+      sql("""
+            |MERGE INTO t
+            |USING t AS source
+            |ON t._ROW_ID = source._ROW_ID
+            |WHEN MATCHED AND source.id IN (2, 6, 7, 9) THEN UPDATE SET t.b = 
source.b + 100
+            |""".stripMargin)
+
+      checkAnswer(
+        sql("SELECT id, b, c, _ROW_ID FROM t ORDER BY id"),
+        Seq(Row(5, 5, 5, 5L), Row(7, 107, 7, 7L), Row(8, 8, 8, 8L)))
+    }
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DeleteFromTableTestBase.scala
 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DeleteFromTableTestBase.scala
index f45a9463b6..2b89afcc68 100644
--- 
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DeleteFromTableTestBase.scala
+++ 
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/DeleteFromTableTestBase.scala
@@ -519,52 +519,6 @@ abstract class DeleteFromTableTestBase extends 
PaimonSparkTestBase {
     }
   }
 
-  test("Paimon delete: data evolution table with deletion vectors") {
-    withTable("t") {
-      sql("""
-            |CREATE TABLE t (id INT, b INT, c INT)
-            |TBLPROPERTIES (
-            |  'row-tracking.enabled' = 'true',
-            |  'data-evolution.enabled' = 'true',
-            |  'deletion-vectors.enabled' = 'true')
-            |""".stripMargin)
-      sql("INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c 
FROM range(0, 5)")
-      sql("INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c 
FROM range(5, 10)")
-      sql("ALTER TABLE t ADD COLUMNS (d INT)")
-      sql(
-        "INSERT INTO t SELECT /*+ REPARTITION(1) */ id, id AS b, id AS c, id + 
100 AS d FROM range(10, 13)")
-
-      sql("DELETE FROM t WHERE id IN (1, 4, 6, 11)")
-      checkAnswer(
-        sql("SELECT *, _ROW_ID FROM t ORDER BY id"),
-        Seq(
-          Row(0, 0, 0, null, 0L),
-          Row(2, 2, 2, null, 2L),
-          Row(3, 3, 3, null, 3L),
-          Row(5, 5, 5, null, 5L),
-          Row(7, 7, 7, null, 7L),
-          Row(8, 8, 8, null, 8L),
-          Row(9, 9, 9, null, 9L),
-          Row(10, 10, 10, 110, 10L),
-          Row(12, 12, 12, 112, 12L)
-        )
-      )
-
-      sql("DELETE FROM t WHERE id IN (2, 8)")
-      checkAnswer(
-        sql("SELECT *, _ROW_ID FROM t ORDER BY id"),
-        Seq(
-          Row(0, 0, 0, null, 0L),
-          Row(3, 3, 3, null, 3L),
-          Row(5, 5, 5, null, 5L),
-          Row(7, 7, 7, null, 7L),
-          Row(9, 9, 9, null, 9L),
-          Row(10, 10, 10, 110, 10L),
-          Row(12, 12, 12, 112, 12L))
-      )
-    }
-  }
-
   test("Paimon delete: delete with range condition") {
     withTable("t") {
       sql(s"CREATE TABLE t (id INT, v INT)")

Reply via email to