This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 2b5f4c080168 [SPARK-60664][SQL] Remap metadata columns in runtime
group filters of group-based UPDATE with subqueries
2b5f4c080168 is described below
commit 2b5f4c080168ad676c291c10dfa2570163659a8d
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Wed Oct 28 20:42:02 2026 -0700
[SPARK-60664][SQL] Remap metadata columns in runtime group filters of
group-based UPDATE with subqueries
### What changes were proposed in this pull request?
This PR aims to fix `RowLevelOperationRuntimeGroupFiltering` to remap the
metadata columns that the UPDATE condition references, in addition to the table
columns, when it builds a runtime group filter.
A group-based UPDATE whose condition has a subquery is rewritten as a
`Union` that reads the table twice
(`RewriteUpdateTable.buildReplaceDataWithUnionPlan`), and
`DeduplicateRelations` gives the second read new expression IDs, including its
metadata attributes. The group filter of each read is the UPDATE condition with
the attributes replaced by name with those of that read. However, only
`write.table.output` (the table columns) has been replaced. When the analyzer
resolves a metadat [...]
For example, `EXPLAIN EXTENDED` shows the following for the second read.
`_partition#30` belongs to the first read.
```
+- Filter dynamicpruningexpression(dep#45 IN (list#177 []))
: +- Aggregate [dep#174], [dep#174]
: +- Project [dep#174]
: +- Join LeftSemi, (pk#172 = pk#53)
: :- !Filter (_partition#30 = hr)
: : +- RelationV2[pk#172, dep#174] cat.ns1.test_table
: +- Project [value#52 AS pk#53]
: +- LocalRelation [value#52]
+- RelationV2[pk#43, salary#44, dep#45, _partition#46, index#47]
cat.ns1.test_table
```
With this PR, the group filter uses the metadata attribute of its own read.
```
+- Filter dynamicpruningexpression(dep#352 IN (list#484 []))
: +- Aggregate [dep#481], [dep#481]
: +- Project [dep#481]
: +- Join LeftSemi, (pk#479 = pk#360)
: :- Project [pk#479, dep#481]
: : +- Filter (_partition#482 = hr)
: : +- RelationV2[pk#479, dep#481, _partition#482]
cat.ns1.test_table
: +- Project [value#359 AS pk#360]
: +- LocalRelation [value#359]
+- RelationV2[pk#350, salary#351, dep#352, _partition#353, index#354]
cat.ns1.test_table
```
The table columns are mapped as before, including the
`_LEGACY_ERROR_TEMP_3075` check.
### Why are the changes needed?
The dangling reference makes such an UPDATE fail at execution with an
internal error. The scan of the group filter subquery prunes `_partition`, and
the post-scan `Filter` cannot bind the reference.
```sql
-- `cat.ns1.test_table` is a group-based table partitioned by `dep`
UPDATE cat.ns1.test_table
SET salary = -1
WHERE _partition = 'hr' AND pk IN (SELECT pk FROM updated_pk)
```
```
org.apache.spark.SparkException: [INTERNAL_ERROR_ATTRIBUTE_NOT_FOUND] Could
not find _partition#26 in [pk#168,dep#170]. SQLSTATE: XX000
```
This happens when the UPDATE is resolved in a single analyzer iteration,
e.g., when no implicit cast is required. Otherwise, `AddMetadataColumns` adds
the metadata columns to the target relation in a later iteration, and they are
mapped as table columns.
### Does this PR introduce _any_ user-facing change?
Yes. Such UPDATE statements fail with an internal error without this PR.
The UPDATE branch of this rule has mapped only the table columns since
SPARK-43975 (Apache Spark 3.5.0).
### How was this patch tested?
Pass the CIs with the newly added test case. Without this fix, the test
fails like the following.
```
$ build/sbt 'sql/testOnly *GroupBasedUpdateTableSuite -- -z SPARK-60664'
...
[info] - SPARK-60664: update runtime group filtering with metadata column
and subquery *** FAILED *** (2 seconds, 467 milliseconds)
[info] org.apache.spark.SparkException: Exception thrown in awaitResult:
...
[info] Cause: org.apache.spark.SparkException:
[INTERNAL_ERROR_ATTRIBUTE_NOT_FOUND] Could not find _partition#26 in
[pk#168,dep#170]. SQLSTATE: XX000
```
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Opus 5.5
Closes #59893 from dongjoon-hyun/SPARK-60664.
Authored-by: Dongjoon Hyun <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
(cherry picked from commit ec018f385fd4aab72933658c976a1bd2c8f7fcb7)
Signed-off-by: Dongjoon Hyun <[email protected]>
---
.../RowLevelOperationRuntimeGroupFiltering.scala | 5 ++++-
.../sql/connector/GroupBasedUpdateTableSuite.scala | 26 ++++++++++++++++++++++
2 files changed, 30 insertions(+), 1 deletion(-)
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/dynamicpruning/RowLevelOperationRuntimeGroupFiltering.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/dynamicpruning/RowLevelOperationRuntimeGroupFiltering.scala
index a26a0cea74f1..06d6d2686f4f 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/dynamicpruning/RowLevelOperationRuntimeGroupFiltering.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/dynamicpruning/RowLevelOperationRuntimeGroupFiltering.scala
@@ -111,7 +111,10 @@ class
RowLevelOperationRuntimeGroupFiltering(optimizeSubqueries: Rule[LogicalPla
// and must transform the runtime filter condition to use correct expr
IDs for each relation
// note this only applies to group-based row-level operations (i.e.
ReplaceData)
// see RewriteUpdateTable for more details
- val attrMap = buildTableToScanAttrMap(write.table.output,
relation.output)
+ // the condition may also reference metadata columns resolved from the
metadata output
+ // of the target relation, which are not in the table output and must
be mapped too
+ val metadataAttrs = (cond.references -- write.table.output).toSeq
+ val attrMap = buildTableToScanAttrMap(write.table.output ++
metadataAttrs, relation.output)
val transformedCond = cond transform {
case attr: AttributeReference if attrMap.contains(attr) =>
attrMap(attr)
}
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/connector/GroupBasedUpdateTableSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/connector/GroupBasedUpdateTableSuite.scala
index 61defff7ebf0..4ce568ed7f68 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/connector/GroupBasedUpdateTableSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/connector/GroupBasedUpdateTableSuite.scala
@@ -168,6 +168,32 @@ class GroupBasedUpdateTableSuite extends
UpdateTableSuiteBase {
}
}
+ test("SPARK-60664: update runtime group filtering with metadata column and
subquery") {
+ withTempView("updated_pk") {
+ createAndInitTable("pk INT NOT NULL, salary INT, dep STRING",
+ """{ "pk": 1, "salary": 300, "dep": "hr" }
+ |{ "pk": 2, "salary": 150, "dep": "software" }
+ |{ "pk": 3, "salary": 120, "dep": "hr" }
+ |""".stripMargin)
+
+ Seq(1, 2).toDF("pk").createOrReplaceTempView("updated_pk")
+
+ // the subquery makes the UPDATE read the table twice (UNION), so the
group filter
+ // of each read must refer to the metadata attributes of that read
+ sql(
+ s"""UPDATE $tableNameAsString
+ |SET salary = -1
+ |WHERE _partition = 'hr' AND pk IN (SELECT pk FROM updated_pk)
+ |""".stripMargin)
+
+ checkAnswer(
+ sql(s"SELECT * FROM $tableNameAsString"),
+ Row(1, -1, "hr") :: Row(2, 150, "software") :: Row(3, 120, "hr") ::
Nil)
+
+ checkReplacedPartitions(Seq("hr"))
+ }
+ }
+
test("update with nondeterministic conditions") {
createAndInitTable("pk INT NOT NULL, id INT, dep STRING",
"""{ "pk": 1, "id": 1, "dep": "hr" }
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]