dwsmith1983 commented on code in PR #5365:
URL: https://github.com/apache/datafusion-comet/pull/5365#discussion_r3861279048


##########
contrib/delta-spark/src/main/scala/org/apache/spark/sql/comet/CometDeltaNativeScanExec.scala:
##########
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.spark.sql.comet
+
+import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.catalyst.expressions._
+import org.apache.spark.sql.catalyst.plans.QueryPlan
+import org.apache.spark.sql.catalyst.plans.physical.{Partitioning, 
UnknownPartitioning}
+import org.apache.spark.sql.execution.{FileSourceScanExec, InSubqueryExec, 
ReusedSubqueryExec, ScalarSubquery, SparkPlan, SubqueryAdaptiveBroadcastExec}
+import org.apache.spark.sql.execution.datasources.HadoopFsRelation
+import org.apache.spark.sql.execution.metric.SQLMetric
+import org.apache.spark.sql.types.StructType
+import org.apache.spark.sql.vectorized.ColumnarBatch
+
+import org.apache.comet.contrib.delta.DeltaSparkScanEnvelope
+import org.apache.comet.serde.OperatorOuterClass
+import org.apache.comet.serde.OperatorOuterClass.Operator
+
+/**
+ * Native scan node for Delta Lake tables (contrib). Delta's own planning (log 
replay, snapshot
+ * resolution, partition pruning) has already run inside delta-spark by the 
time this node is
+ * created from the DSv1 [[FileSourceScanExec]]; file listing and split 
planning are delegated to
+ * a [[CometScanExec]] helper, and data reads execute through Comet's native 
DataFusion parquet
+ * machinery, inheriting row-group and page-index pruning.
+ *
+ * DPP: `runtimeFilters` is a constructor field included in equality, so
+ * `CometPlanAdaptiveDynamicPruningFilters`'s rewrite (via 
[[CometScanWithPlanData]]) survives
+ * plan copies, the lesson from CometIcebergNativeScanExec (a transient field 
is dropped by
+ * `TreeNode.makeCopy` on MERGE re-planning).
+ */
+case class CometDeltaNativeScanExec(
+    override val nativeOp: Operator,
+    override val output: Seq[Attribute],
+    requiredSchema: StructType,
+    runtimeFilters: Seq[Expression],
+    dataFilters: Seq[Expression],
+    @transient relation: HadoopFsRelation,
+    originalPlan: FileSourceScanExec,
+    override val serializedPlanOpt: SerializedPlan,
+    sourceKey: String)
+    extends CometLeafExec
+    with CometScanWithPlanData {
+
+  override val nodeName: String = s"CometDeltaNativeScan $relation"
+
+  // Derived from (originalPlan, runtimeFilters), never stored: any copy of 
this node, our
+  // own withDynamicPruningFilters, or a generic Catalyst expression rewrite 
going through
+  // TreeNode.makeCopy, automatically gets a helper consistent with ITS 
runtimeFilters. A
+  // stored helper field would desync from rewritten filters (the #3510 class 
of bug). The
+  // cost is that file listing runs once per executed instance (planning 
listed separately in
+  // the rule extension); correctness over the duplicate driver-side listing.
+  //
+  // Forcing invariant: this lazy val is forced by the `metrics` override 
below (via
+  // `scanHelper.metrics`), and AQE's UI plan-walk calls `.metrics` on every 
node MID-PLANNING --
+  // including while a DPP subquery is still an adaptive placeholder or a 
partition filter holds
+  // an unresolved ScalarSubquery (see `hasUnevaluableSubqueryFilter` just 
below). Constructing
+  // `scanHelper` here, and reading `CometScanExec.metrics` off it, is safe 
ONLY because that
+  // construction is a cheap case-class build with no file listing, and core's
+  // `CometScanExec.metrics` (spark/.../CometScanExec.scala) touches only 
`wrapped.driverMetrics`
+  // -- populated by Spark's own planning, not by this scan -- plus a static 
metric-node
+  // constructor. Neither does file listing 
(`selectedPartitions`/`getFilePartitions`) or
+  // subquery resolution. If core's `metrics` is ever changed to touch either, 
forcing
+  // `scanHelper` from this `metrics` override would resurrect the two AQE 
mid-planning crashes
+  // this invariant was written to prevent.
+  @transient private lazy val scanHelper: CometScanExec =
+    CometDeltaNativeScanExec.planningHelper(originalPlan, runtimeFilters)
+
+  // NOT lazy val: while a DPP subquery is still an adaptive placeholder, or a 
partition filter
+  // holds an unresolved scalar subquery, this returns a temporary value that 
must not be
+  // memoized -- after CometPlanAdaptiveDynamicPruningFilters rewrites the 
filters (DPP case) or
+  // AQE resolves the subquery (scalar case), later reads must see the real 
post-pruning
+  // partition count.
+  override def outputPartitioning: Partitioning =
+    if (hasUnevaluableSubqueryFilter) UnknownPartitioning(0)
+    else UnknownPartitioning(perPartitionData.length)
+
+  // runtimeFilters IS scanHelper.partitionFilters element-for-element 
(planningHelper passes
+  // partitionFilters = runtimeFilters into CometScanExec's plain constructor 
field below), so
+  // checking runtimeFilters here avoids constructing/forcing the derived 
scanHelper just to
+  // read partitioning. The InSubqueryExec placeholder shapes mirror
+  // CometPlanAdaptiveDynamicPruningFilters.extractSABData + hasWrappedSAB -- 
keep these two
+  // sets in sync; if that rule learns to unwrap a new wrapper form, mirror it 
here too. The
+  // ScalarSubquery case (e.g. `p = (SELECT max(p) FROM dim ...)`, a 
partition-column filter,
+  // so it lands in runtimeFilters rather than dataFilters) is presence-based, 
unlike the
+  // InSubqueryExec check above: Spark exposes no public finished/updated 
probe on
+  // ExecSubqueryExpression, so any partition filter containing one forces 
UnknownPartitioning(0)
+  // even once it has actually resolved. That is safe one-directionally only 
-- worst case an
+  // extra shuffle from an overly conservative partitioning, never a wrong 
answer, since
+  // execution reads perPartitionData directly and never goes through this 
getter.
+  private def hasUnevaluableSubqueryFilter: Boolean =
+    runtimeFilters.exists(_.exists {
+      // Match `e: InSubqueryExec` and dispatch on e.plan rather than 
unapplying InSubqueryExec
+      // directly: its unapply arity differs across Spark versions and this 
module ships no
+      // version shim.
+      case e: InSubqueryExec => isAdaptivePlaceholder(e.plan)
+      case _: ScalarSubquery => true

Review Comment:
   Fixed. The guard now probes resolution instead of presence: eval on a 
resolved scalar subquery is a pure cached read (verified across the 3.5 through 
4.2 jars), so once prepare resolves it the getter returns the real partition 
count, and only the unresolved case reports UnknownPartitioning(0). Your 
fused-parent shape reproduced the per-partition length failure locally and is 
now a regression that requires the native scan beneath a native aggregate.



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