Skip to content

Commit 1db1fd2

Browse files
leaves12138lirui-apache
authored andcommitted
[spark] Fix merge into update columns detection (#7868)
This updates Spark merge-into data evolution update column detection so target self-assignments are not treated as modified columns when Spark adds or changes qualifiers. The logic now treats matching AttributeReference exprIds as the same target column, while still including source-side assignments such as target_col = source_col.
1 parent 981ae4e commit 1db1fd2

3 files changed

Lines changed: 84 additions & 6 deletions

File tree

‎paimon-flink/paimon-flink-common/src/test/java/org/apache/paimon/flink/action/DataEvolutionMergeIntoActionITCase.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ public void testUpdateNonBlobColumnOnRawBlobTableWithSplitFiles() throws Excepti
735735
put(DATA_EVOLUTION_ENABLED.key(), "true");
736736
put("blob-field", "picture");
737737
put(CoreOptions.BLOB_TARGET_FILE_SIZE.key(), "1 b");
738+
put("sink.parallelism", "1");
738739
}
739740
}));
740741
insertInto(

‎paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/commands/MergeIntoPaimonDataEvolutionTable.scala‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,8 @@ case class MergeIntoPaimonDataEvolutionTable(
8686
action match {
8787
case updateAction: UpdateAction =>
8888
for (assignment <- updateAction.assignments) {
89-
if (!assignment.key.equals(assignment.value)) {
90-
val key = assignment.key.asInstanceOf[AttributeReference]
91-
columns ++= Seq(key)
89+
if (isModifiedAssignment(assignment)) {
90+
columns += assignmentKeyAttribute(assignment)
9291
}
9392
}
9493
}
@@ -281,9 +280,7 @@ case class MergeIntoPaimonDataEvolutionTable(
281280
UpdateAction.apply(
282281
update.condition,
283282
update.assignments.filter(
284-
a =>
285-
updateColumnsSorted.contains(
286-
a.key.asInstanceOf[AttributeReference])) ++ assignments))
283+
a => updateColumnsSorted.contains(assignmentKeyAttribute(a))) ++ assignments))
287284

288285
for (action <- realUpdateActions) {
289286
allFields ++= action.references.flatMap(r => extractFields(r)).seq
@@ -596,6 +593,27 @@ object MergeIntoPaimonDataEvolutionTable {
596593
final private val ROW_ID_NAME = "_ROW_ID"
597594
final private val FIRST_ROW_ID_NAME = "_FIRST_ROW_ID";
598595

596+
private[commands] def isModifiedAssignment(assignment: Assignment): Boolean = {
597+
!sameAttributeReference(assignment.key, assignment.value)
598+
}
599+
600+
private[commands] def assignmentKeyAttribute(assignment: Assignment): AttributeReference = {
601+
assignment.key match {
602+
case key: AttributeReference => key
603+
case other =>
604+
throw new UnsupportedOperationException(
605+
s"Unsupported update assignment key: $other. Only top-level attributes are supported.")
606+
}
607+
}
608+
609+
private def sameAttributeReference(left: Expression, right: Expression): Boolean = {
610+
(left, right) match {
611+
case (leftAttr: AttributeReference, rightAttr: AttributeReference) =>
612+
leftAttr.sameRef(rightAttr)
613+
case _ => false
614+
}
615+
}
616+
599617
private def floorBinarySearch(indexed: immutable.IndexedSeq[Long], value: Long): Long = {
600618
if (indexed.isEmpty) {
601619
throw new IllegalArgumentException("The input sorted sequence is empty.")
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.paimon.spark.commands
20+
21+
import org.apache.spark.SparkFunSuite
22+
import org.apache.spark.sql.catalyst.expressions.{AttributeReference, GetStructField, Literal}
23+
import org.apache.spark.sql.catalyst.plans.logical.Assignment
24+
import org.apache.spark.sql.types.{BinaryType, StringType, StructField, StructType}
25+
26+
class MergeIntoPaimonDataEvolutionTableTest extends SparkFunSuite {
27+
28+
test("update column detection ignores target self-assignment with different qualifiers") {
29+
val targetPicture = AttributeReference("picture", BinaryType)()
30+
val qualifiedTargetPicture = targetPicture.withQualifier(Seq("t"))
31+
32+
assert(!targetPicture.equals(qualifiedTargetPicture))
33+
assert(targetPicture.sameRef(qualifiedTargetPicture))
34+
assert(
35+
!MergeIntoPaimonDataEvolutionTable.isModifiedAssignment(
36+
Assignment(targetPicture, qualifiedTargetPicture)))
37+
}
38+
39+
test("update column detection includes source assignment with same column name") {
40+
val targetFileType = AttributeReference("file_type", StringType)()
41+
val sourceFileType = AttributeReference("file_type", StringType)().withQualifier(Seq("s"))
42+
43+
assert(!targetFileType.sameRef(sourceFileType))
44+
assert(
45+
MergeIntoPaimonDataEvolutionTable.isModifiedAssignment(
46+
Assignment(targetFileType, sourceFileType)))
47+
}
48+
49+
test("update column detection rejects non top-level assignment key") {
50+
val targetStruct =
51+
AttributeReference("metadata", StructType(Seq(StructField("name", StringType))))()
52+
val nestedKey = GetStructField(targetStruct, 0, Some("name"))
53+
54+
intercept[UnsupportedOperationException] {
55+
MergeIntoPaimonDataEvolutionTable.assignmentKeyAttribute(
56+
Assignment(nestedKey, Literal("new-name")))
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)