LuciferYang commented on code in PR #55579:
URL: https://github.com/apache/spark/pull/55579#discussion_r4056395112


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/FileScan.scala:
##########
@@ -186,6 +209,47 @@ trait FileScan extends Scan
     partitions.toArray
   }
 
+  /**
+   * The partition columns Spark can derive a runtime filter on (SPARK-30628), 
restricted to the
+   * ones `readSchema()` still exposes: a reference missing from the scan 
relation output fails to
+   * resolve, and a pushed-down aggregate keeps only the partition columns it 
groups by.
+   *
+   * A filter over one of them is applied by selecting partition directories 
in `buildPartitions`,
+   * the same treatment a compile-time `partitionFilters` entry gets, so the 
scan evaluates it in
+   * full and Spark does not evaluate it again after the scan -- 
`FileScanBuilder.pushFilters`
+   * already keeps compile-time partition filters out of the post-scan filters 
for that reason.
+   * Directory selection matches a predicate's references against 
`fileIndex.partitionSchema` by
+   * name, so the names reported here have to be the ones that schema uses; 
both sides derive from
+   * it today, and a divergence would silently drop the predicate instead of 
failing.
+   */
+  override def filterAttributes(): Array[NamedReference] = {
+    val readFields = readSchema().fieldNames.map(normalizeName).toSet
+    readPartitionSchema.fieldNames
+      .filter(name => readFields.contains(normalizeName(name)))
+      .map(FieldReference.column)
+  }
+
+  override def fullyPushedFilterAttributes(): Array[NamedReference] = 
filterAttributes()
+
+  override def planInputPartitionsWithRuntimeFilters(
+      expressions: Array[Expression]): Array[InputPartition] = {
+    // Directory selection is the only thing that applies these, and it 
silently ignores a predicate
+    // whose references are not all partition columns -- which, for attributes 
declared fully
+    // pushed, would leave the predicate evaluated nowhere. Spark screens for 
that before it gets
+    // here; fail loudly rather than return wrong rows if that ever stops 
being true. Compare the
+    // way the file index does, by exact name against its own partition 
schema, so this guard cannot
+    // pass something the index will then drop.
+    val partitionNames = fileIndex.partitionSchema.fieldNames.toSet
+    val notApplicable = expressions.filterNot(
+      _.references.forall(a => partitionNames.contains(a.name)))
+    if (notApplicable.nonEmpty) {
+      throw SparkException.internalError("A file scan can only apply a runtime 
filter over its " +
+        s"partition columns 
${fileIndex.partitionSchema.fieldNames.mkString("[", ", ", "]")}, " +
+        s"got ${notApplicable.mkString(", ")}")
+    }
+    buildPartitions(partitionFilters ++ expressions).toArray

Review Comment:
   You are right, and my earlier answer in this thread was wrong in the case 
that matters. It said an override of `partitions` keeps working because the 
compile-time path still calls it, which only holds while no runtime filter 
fires, and a runtime filter firing is the whole point of the feature.
   
   Taken, in the shape you proposed. `planInputPartitionsWithRuntimeFilters` 
now filters what `partitions` already planned instead of listing again: it 
binds the expressions against `readPartitionSchema`, drops each 
`PartitionedFile` whose `partitionValues` do not satisfy them, and drops the 
partitions left empty.
   
   ```scala
   val predicate = Predicate.createInterpreted(bound)
   partitions.flatMap { part =>
     val kept = part.files.filter(file => predicate.eval(file.partitionValues))
     if (kept.isEmpty) None else Some(part.copy(files = kept))
   }.toArray
   ```
   
   Three things fall out of it. An override of `partitions` is honored, since 
its files are the only ones reachable from here. `partitions` is memoized, so 
both read paths share one listing and the duplicate driver-side listing and 
packing is gone, which takes the first item in the description's list of things 
this PR does not address with it. And the `buildPartitions` seam this PR had 
added is no longer needed, so `partitions` goes back to what it was on master.
   
   The guard stays, and now compares against `readPartitionSchema`, which is 
what the binding uses. It matches names the way the rest of the scan does, so a 
mixed-case reference binds rather than being rejected.
   
   New test: `a subclass that narrows the file set in partitions keeps it under 
a runtime filter` builds a `ParquetScan` subclass whose `partitions` keeps one 
directory's files, then asks it for a filter selecting a different directory. 
Reverting the method to the listing-based version turns it red by handing back 
exactly the files the override excluded:
   
   ```
   Array(FilePartition(0, Array(... /fact/part=7/part-00000-....parquet, 
partition values: [7])),
         FilePartition(1, Array(... /fact/part=7/part-00001-....parquet, 
partition values: [7]))) was not empty
   ```
   
   The same test asserts the scan asks for its partitions once across both 
calls; dropping the memoization turns that into `2 did not equal 1`.
   
   The description's user-facing section now also says that the scan is the 
only evaluator of such a filter, and that a file whose partition value cannot 
be evaluated fails the query.
   



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