dongjoon-hyun opened a new pull request, #59920:
URL: https://github.com/apache/spark/pull/59920

   [SPARK-60710][SQL] Expose source metadata columns to the runtime group 
filter of DSv2 MERGE
   
   ### What changes were proposed in this pull request?
   
   This PR aims to make the runtime group filter of DSv2 MERGE read the source 
metadata columns that the `ON` condition refers to.
   
   `RewriteMergeIntoTable.toGroupFilterCondition` converts the `ON` condition 
into `EXISTS (SELECT * FROM source WHERE <ON condition>)` on top of the 
original source plan. A source metadata column (e.g. `s._partition` of a DSv2 
table or `s._metadata` of a file source table) is resolved from 
`source.metadataOutput`, so the source plan does not output it yet when the 
MERGE is rewritten. For the main join, `Analyzer.AddMetadataColumns` adds the 
column to the source in the next analyzer iteration, but the rule does not 
reach the subquery plan. In addition, `DeduplicateRelations` assigns new 
expression IDs to the subquery's copy of the source relation, so the subquery 
keeps a reference to the `_partition` attribute of the main join's source.
   
   ```
   ReplaceData RelationV2[pk#22, salary#23, dep#24] cat.ns1.test_table
   :  +- !Filter ((outer(pk#22) = pk#56) AND (_partition#30 = hr))
   :     +- SubqueryAlias s
   :        +- SubqueryAlias cat.ns1.source_table
   :           +- RelationV2[pk#56, dep#57] cat.ns1.source_table
   +- MergeRows[...]
      +- Join LeftOuter, ((pk#22 = pk#25) AND (_partition#30 = hr)), ...
         :- ...
         +- Project [pk#25, dep#26, true AS __row_from_source#34, _partition#30]
            +- SubqueryAlias s
               +- SubqueryAlias cat.ns1.source_table
                  +- RelationV2[pk#25, dep#26, index#29, _partition#30] 
cat.ns1.source_table
   ```
   
   This PR adds the referenced source metadata columns to the source plan of 
the subquery in the same way as `AddMetadataColumns` does, i.e. 
`ExposesMetadataColumns.withMetadataColumns()` for the relation and extra 
project list entries for the `Project` nodes in between. The source plan is 
unchanged when the `ON` condition does not refer to source metadata columns.
   
   ```scala
   +    // the condition may refer to source metadata columns that are not in 
the source output
   +    // AddMetadataColumns adds them to the source in the join but does not 
reach this subquery
   +    val sourceMetadataAttrs =
   +      AttributeSet(source.metadataOutput).intersect(cond.references) -- 
source.outputSet
   +    val filterSource = if (sourceMetadataAttrs.isEmpty) {
   +      source
   +    } else {
   +      addMetadataAttrs(source, sourceMetadataAttrs)
   +    }
   +    Exists(Filter(condWithOuterRefs, filterSource), outerRefs)
   ```
   
   Since the subquery's source relation now outputs the column, 
`DeduplicateRelations` renews it together with the other columns, and the group 
filter applies the source predicate.
   
   ```
   +- BroadcastHashJoin [pk#4009], [pk#3879], LeftSemi, BuildRight, false, false
      :- Project [pk#4009, dep#4011]
      :  +- BatchScan cat.ns1.test_table[pk#4009, dep#4011] ...
      +- BroadcastQueryStage 0
         +- BroadcastExchange ...
            +- *(1) Project [pk#3879]
               +- *(1) Filter ((_partition#3882 = hr) AND isnotnull(pk#3879))
                  +- BatchScan cat.ns1.source_table[pk#3879, _partition#3882] 
...
   ```
   
   Two simpler alternatives were considered and not chosen.
   - Skip the group filter when the `ON` condition refers to source metadata 
columns. This is what disabling the group filter does today, and a group-based 
MERGE reads and replaces every group of the target table.
   - Drop the predicates on source metadata columns from the group filter. The 
filter still selects every group with matching rows, but it can select more. In 
the new test, the `software` partition would be replaced although none of its 
rows changes.
   
   ### Why are the changes needed?
   
   A DSv2 MERGE whose `ON` condition refers to a metadata column of the source 
fails at analysis with an internal error when the runtime group filter is used, 
i.e. `spark.sql.optimizer.runtime.rowLevelOperationGroupFilter.enabled` is 
`true` (default) and there is no `WHEN NOT MATCHED BY SOURCE` clause.
   
   ```sql
   CREATE TABLE cat.ns1.source_table (pk INT, dep STRING) PARTITIONED BY (dep);
   INSERT INTO cat.ns1.source_table VALUES (1, 'hr'), (3, 'software'), (5, 
'hr');
   
   MERGE INTO cat.ns1.test_table t
   USING cat.ns1.source_table s
   ON t.pk = s.pk AND s._partition = 'hr'
   WHEN MATCHED THEN
    UPDATE SET t.salary = -1
   ```
   
   ```
   org.apache.spark.sql.catalyst.ExtendedAnalysisException: 
[MISSING_ATTRIBUTES.RESOLVED_ATTRIBUTE_MISSING_FROM_INPUT] Resolved 
attribute(s) "_partition" missing from "pk", "dep" in operator !Filter 
((outer(pk#22) = pk#56) AND (_partition#30 = hr)).  SQLSTATE: XX000; line 1 pos 
0;
   ```
   
   The same statement succeeds when the runtime group filter is disabled. A 
MERGE that is rewritten in a later analyzer iteration, e.g. because its `ON` 
condition needs type coercion, does not fail either, because 
`AddMetadataColumns` adds the column to the MERGE source before the rewrite. 
The subquery has been built on top of the original source plan since the 
runtime group filter of group-based MERGE was added in Apache Spark 
[3.5.0](https://github.com/apache/spark/releases/tag/v3.5.0) (2023-09-09, 
SPARK-43963), and delta-based MERGE uses it since Apache Spark 
[4.2.0](https://github.com/apache/spark/releases/tag/v4.2.0) (2026-07-11, 
SPARK-56669).
   
   This is the source-side counterpart of SPARK-60668 (apache/spark#59896), 
which made the runtime group filter treat the target metadata columns in the 
same condition as outer references.
   
   ### Does this PR introduce _any_ user-facing change?
   
   Yes. Such MERGE statements succeed instead of failing with the internal 
error above, and the runtime group filter honors the predicates on source 
metadata columns.
   
   ### How was this patch tested?
   
   Pass the CIs with the newly added test cases. They fail without this fix. 
Each test runs the statement with the DSv2 source table and with a subquery on 
top of it, which covers the `Project` case. The group-based test also checks 
that only the `hr` partition is replaced.
   
   ```
   $ build/sbt 'sql/testOnly *GroupBasedMergeIntoTableSuite 
*DeltaBasedMergeIntoTableSuite -- -z SPARK-60710'
   ```
   
   ### Was this patch authored or co-authored using generative AI tooling?
   
   Generated-by: Claude Opus 5.5


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to