weiqingy commented on code in PR #2456:
URL: https://github.com/apache/auron/pull/2456#discussion_r3746764288
##########
thirdparty/auron-iceberg/src/main/scala/org/apache/spark/sql/auron/iceberg/IcebergScanSupport.scala:
##########
@@ -290,7 +320,10 @@ object IcebergScanSupport extends Logging {
}
val pruningPredicates =
collectPruningPredicates(scan.asInstanceOf[AnyRef], readSchema)
- val nativeTasks = nativeChangelogTasks.map(task => toNativeScanTask(task,
partitionSchema))
+ val filteredTasks = exec
+ .getTagValue(changelogTaskFilterTag)
Review Comment:
The tag read here goes missing whenever the scan is rebuilt for runtime
filters. `withRuntimeFilters` (line 145 in this file) calls
`Shims.copyBatchScanExecWithRuntimeFilters`, which builds the new node with the
Scala case-class `.copy`. Spark's own doc on `TreeNode.tags` (3.5.8
`TreeNode.scala:72-74`) says tags carry over only "when this node is copied via
`makeCopy`, or transformed via `transformUp`/`transformDown`", and a plain
`.copy` is none of those. The new node starts with an empty tag map, so
`changelogTaskFilterTag` is gone.
So on a changelog scan carrying runtime filters, which is the shape the
existing `iceberg native changelog scan remains correct in dynamic pruning
join` test sets up, the pruning quietly does nothing. It fails in the safe
direction, but nothing logs that it happened.
Was that intentional? If not, would it make sense for `withRuntimeFilters`
to carry the tag onto the new node, or at least log when it drops it?
##########
thirdparty/auron-iceberg/src/main/scala/org/apache/spark/sql/auron/iceberg/IcebergConvertProvider.scala:
##########
@@ -28,6 +29,25 @@ import org.apache.auron.util.SemanticVersion
class IcebergConvertProvider extends AuronConvertProvider with Logging {
+ override def prepare(exec: SparkPlan): Unit = {
+ exec.foreach {
+ case filter: FilterExec
+ if
IcebergScanSupport.isSupportedChangelogTaskFilter(filter.condition) =>
+ val referencedNames = filter.condition.references.map(_.name).toSet
Review Comment:
`filter.condition.references` is an `AttributeSet`, so it already carries
each attribute's `exprId`. Mapping it to `_.name` throws that away, and the
check then only asks whether some changelog scan below happens to expose
columns with those names, not whether the filter's attributes actually come
from that scan.
Two ways that can bite. An alias that reuses the column name passes: in
`select max(_change_ordinal) as _change_ordinal from v`, the aggregate's output
attribute has a different exprId from the scan's but the same name. And these
three names are not reserved by Iceberg. `MetadataColumns.META_COLUMNS`
(Iceberg 1.10.1, `MetadataColumns.java:110-117`) does not list them, so a user
table can declare its own `_commit_snapshot_id`. A filter on that column above
a full outer join, with a changelog scan on the other side, would tag and prune
the changelog scan. For the other join types Spark pushes a single-side
predicate below the join (`PushPredicateThroughJoin.canPushThrough`), so full
outer is the one that reaches `prepare`.
Would `filter.condition.references.subsetOf(scan.outputSet)` work here?
##########
thirdparty/auron-iceberg/src/main/scala/org/apache/spark/sql/auron/iceberg/IcebergConvertProvider.scala:
##########
@@ -28,6 +29,25 @@ import org.apache.auron.util.SemanticVersion
class IcebergConvertProvider extends AuronConvertProvider with Logging {
+ override def prepare(exec: SparkPlan): Unit = {
+ exec.foreach {
+ case filter: FilterExec
+ if
IcebergScanSupport.isSupportedChangelogTaskFilter(filter.condition) =>
+ val referencedNames = filter.condition.references.map(_.name).toSet
+ val changelogScans = filter.child.collect {
Review Comment:
`filter.child.collect` searches the whole subtree below the filter, so any
operator can sit between the filter and the scan.
That matters because dropping tasks at the scan is only safe if the filter
could have been evaluated at the scan in the first place. Spark already decides
that, and a filter still sitting above an operator is usually one Spark refused
to push down. In Spark 3.5.8 `Optimizer.scala`,
`PushPredicateThroughNonJoin.canPushThrough` has no case for `Limit`, and the
rule's separate `Aggregate` case only fires when `groupingExpressions.nonEmpty`.
Here is a shape that looks like it would return a wrong answer. On a
changelog view `v` with change ordinals 0, 1, 2:
```sql
select * from (select max(_change_ordinal) as _change_ordinal from v) where
_change_ordinal = 0
```
There are no grouping keys, so the filter stays above the aggregate.
`collect` still reaches the changelog scan and tags it, the scan then reads
only the ordinal-0 task, `max(...)` returns 0, the filter passes, and the query
returns `Row(0)`. Without pruning `max(...)` is 2 and the result is empty. The
new test never puts a filter above a non-adjacent operator, so this would not
go red today.
A filter above a `limit`, or above an unpartitioned `count(*) over ()`,
looks like the same family. The window case is worth calling out separately:
there is no alias involved, so the exprIds line up and tightening the name
match on line 36 would not catch it.
I traced this through the optimizer source rather than running it, so I may
be missing something that keeps these plans away from `prepare`.
Would it help to match only a filter that sits directly above the scan, with
`Project`s allowed in between? Something like this, though happy to be
redirected:
```scala
def scanUnder(p: SparkPlan): Option[BatchScanExec] = p match {
case s: BatchScanExec => Some(s)
case proj: ProjectExec => scanUnder(proj.child)
case _ => None
}
```
##########
spark-extension/src/main/scala/org/apache/spark/sql/auron/AuronSparkSessionExtension.scala:
##########
@@ -37,6 +37,7 @@ class AuronSparkSessionExtension extends
(SparkSessionExtensions => Unit) with L
logInfo(s"${classOf[AuronSparkSessionExtension].getName} enabled")
Shims.get.onApplyingExtension()
+ Shims.get.injectQueryStagePrepRule(extensions)
Review Comment:
There may already be a place to do this. `preColumnarTransitions` in this
file receives the whole stage plan, and it already runs a whole-plan pass at
line 86 (`AuronConvertStrategy.apply(sparkPlan)`) before converting at line 90.
Tags set in a pass like that survive, because for a leaf `BatchScanExec`
`withNewChildren(Nil)` returns the same object, so the converter later reads
the node that was tagged.
Calling `AuronConverters.prepareExtensionPlans(sparkPlan)` just before line
86 looks like it would do the same job, with no new `Shims` method and no new
`SparkSessionExtensions` injection point. It would also run per query stage,
which narrows the subtree search I mentioned in
`IcebergConvertProvider.prepare`, since each of those shapes has an exchange
between the filter and the scan. That is a narrowing though, not a replacement
for the adjacency and exprId checks.
Is there something about the pre-stage AQE hook that is needed here?
--
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]