dongjoon-hyun commented on code in PR #58503:
URL: https://github.com/apache/spark/pull/58503#discussion_r3927706529


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala:
##########
@@ -841,14 +837,14 @@ abstract class InMemoryBaseTable(
     var pushedFilters: Array[Filter] = Array.empty
 
     override def filterAttributes(): Array[NamedReference] = {
-      partitioning.flatMap(_.references)
+      identityPartitionReferences

Review Comment:
   Nit: this 
`identityPartitionReferences.filter(readSchema.findNestedField(...).isDefined)` 
expression is now copied in four scans (here, `InMemoryTableWithV2Filter`, 
`InMemoryRowLevelOperationTable`, and 
`InMemoryCatalystRuntimeFilterTable.identityPartitionAttrs`), and only the last 
one carries `.distinct`. A single protected helper on `BatchScanBaseClass` next 
to `identityPartitionReferences` would remove the drift.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala:
##########
@@ -212,8 +212,11 @@ object PushDownUtils extends Logging {
         // filters whose translation was not already accepted in the first 
pass.  (See SPARK-55596)
         // Only candidates whose referenced columns are declared in 
filterAttributes() are eligible.
         val partPredicatesPushed = filterableScan.supportsIterativePushdown() 
&& {
-          val filterAttrs = V2ExpressionUtils.resolveAttributeRefs(
-            filterableScan.filterAttributes(), output)
+          val filterAttrs = DataSourceV2ScanRelation.resolveRuntimeFilterAttrs(

Review Comment:
   With this and the two `PartitionPruning` call sites rewired, 
`V2ExpressionUtils.resolveAttributeRefs` has no remaining callers, and the new 
companion helper re-implements its body plus the error wrapping. Leaving both 
around means a future call site can pick the public `V2ExpressionUtils` one and 
surface `_LEGACY_ERROR_TEMP_1137` for the same misdeclaration every other path 
now reports as `DATA_SOURCE_INVALID_RUNTIME_FILTER_ATTRIBUTE.CANNOT_RESOLVE`.
   
   Either remove it (it shipped in 4.2.0 on a public object, so worth a note in 
the PR description) or make it the home of the error-wrapping version and drop 
the new `object DataSourceV2ScanRelation`.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Relation.scala:
##########
@@ -326,6 +315,17 @@ case class DataSourceV2ScanRelation(
         scanClass = scan.getClass.getName,
         relationOutput = fromAttributes(output))
     }
+    declaredFullyPushedRuntimeFilterAttrs.find { fullyPushedRef =>

Review Comment:
   This name-only membership check runs before `fullyPushedFilterAttributes()` 
is resolved against the output. A fully pushed reference that does not exist at 
all is therefore reported as `NOT_IN_FILTER_ATTRIBUTES` ("must also be returned 
by `filterAttributes()`") rather than `CANNOT_RESOLVE`. That is exactly the 
`MissingFullyPushedFilterAttributeScan` case, whose expectation was flipped in 
this PR and whose `getCause` assertion was dropped.
   
   A connector author following that message would add `missing` to 
`filterAttributes()` and only then get `CANNOT_RESOLVE` on the next run. 
Checking resolvability of the fully pushed refs before the membership check (or 
resolving both lists first) would surface the root cause in one round and let 
the original test expectation stand.



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/dynamicpruning/RowLevelOperationRuntimeGroupFiltering.scala:
##########
@@ -55,32 +55,32 @@ class 
RowLevelOperationRuntimeGroupFiltering(optimizeSubqueries: Rule[LogicalPla
   override def apply(plan: LogicalPlan): LogicalPlan = 
plan.transformDownWithPruning(
       _.containsAnyPattern(REPLACE_DATA, WRITE_DELTA)) {
     case GroupBasedRowLevelOperation(replaceData, _, Some(cond),
-        ExtractV2Scan(scan: SupportsRuntimeV2Filtering))
-        if canInjectGroupFilters(cond, scan.filterAttributes) =>
+        r @ ExtractV2Scan(scan: SupportsRuntimeV2Filtering))

Review Comment:
   Nit: now that eligibility comes from `r.runtimeFilterAttrs`, the four arms 
here differ only by the interface type test and `filterAttributes` vs 
`filterAttributes()`, and the two V2 arms in 
`PartitionPruning.getFilterableTableScan` are identical with `scan` unused. 
`declaredRuntimeFilterAttrs` already folds both interfaces (and the 
neither-interface case) into one array.
   
   `PartitionPruning` could collapse to a single `case (resExp, r: 
DataSourceV2ScanRelation) if resExp.references.subsetOf(r.runtimeFilterAttrs)`, 
and this rule to two arms (Group/Delta) taking `r` and using `r.scan` plus a 
`private[sql]` `declaredRuntimeFilterAttrs` for the raw `NamedReference` array 
that `injectGroupFilters` still needs for nested paths. A scan implementing 
neither interface yields an empty set without throwing, so this is 
behavior-preserving.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala:
##########
@@ -841,14 +837,14 @@ abstract class InMemoryBaseTable(
     var pushedFilters: Array[Filter] = Array.empty
 
     override def filterAttributes(): Array[NamedReference] = {
-      partitioning.flatMap(_.references)
+      identityPartitionReferences
         .filter(ref => readSchema.findNestedField(
           ref.fieldNames.toImmutableArraySeq, resolver = 
SQLConf.get.resolver).isDefined)
     }
 
     override def filter(filters: Array[Filter]): Unit = {
-      if (partitioning.length == 1 && partitioning.head.references().length == 
1) {
-        val ref = partitioning.head.references().head
+      if (partitioning.length == 1 && identityPartitionReferences.length == 1) 
{

Review Comment:
   `InMemoryScanBuilder.canEvaluate` (line 519) still uses `partitioning.length 
== 1 && partitioning.head.references.length == 1`, so an `In` on a single 
non-identity transform (e.g. `PARTITIONED BY (days(part))`) is still classified 
as fully evaluable and removed from `postScanFilters` by `pushFilters`. With 
this guard now identity-only, `build()` hands that `In` to `filter()` and it is 
skipped, so the static filter is evaluated nowhere.
   
   Example: `CREATE TABLE t (id INT, part DATE) PARTITIONED BY (days(part))`, 
two dates inserted, `SELECT * FROM t WHERE part IN (DATE '2026-08-01')` now 
returns both rows.
   
   Could we make `canEvaluate` use the same identity-only guard 
(`identityPartitionReferences.length == 1`) so the two sides agree?



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/execution/datasources/v2/DataSourceV2Relation.scala:
##########
@@ -347,6 +347,30 @@ case class DataSourceV2ScanRelation(
   }
 }
 
+object DataSourceV2ScanRelation {
+  private[sql] def resolveRuntimeFilterAttrs(
+      filterAttrs: Array[NamedReference],
+      method: String,
+      scanClass: String,
+      output: Seq[AttributeReference]): AttributeSet = {
+    val plan = LocalRelation(output)

Review Comment:
   Nit: `LocalRelation(output)` is built even when `filterAttrs` is empty, and 
the instance path now forwards here instead of resolving against `this` as 
before. `DataSourceV2Strategy` reads `runtimeFilterAttrs` for every V2 scan 
relation, including scans with no runtime-filter interface, so each of those 
now allocates two `LocalRelation`s for nothing. Small cost, but a one-line `if 
(filterAttrs.isEmpty) return AttributeSet.empty` short-circuit (or resolving 
against `this` in the instance path) avoids it.



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2CatalystRuntimeFilterSuite.scala:
##########
@@ -432,30 +475,28 @@ class DataSourceV2CatalystRuntimeFilterSuite extends 
SharedSparkSession {
     }
   }
 
-  test("filter on column outside filterAttributes -> not pushed, even if 
declared fully pushed") {
+  test("fully pushed attribute outside filterAttributes -> rejected") {

Review Comment:
   Turning the old tbl4 test into a pure rejection test drops the suite's only 
coverage of the valid case: a scalar-subquery filter on a partition column 
outside a restricted `filterAttributes()` must not be routed as a runtime 
filter, `filter()` must not be called, and the post-scan `FilterExec` must be 
kept. `'filter-attributes'` now only appears in this invalid-declaration test.
   
   Could we keep a sibling test with `'filter-attributes' = 'p1'` and no 
fully-pushed property, asserting `runtimeFilters.isEmpty`, 
`assertPushedCatalystPredicates(df, 0)`, 
`assertScalarSubqueryEvaluatedAfterScan(df, expected = true)` and all 5 
partitions retained?



-- 
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