comphead commented on code in PR #7343:
URL: https://github.com/apache/datafusion-comet/pull/7343#discussion_r4485785207


##########
spark/src/main/scala/org/apache/comet/rules/CometScanRule.scala:
##########
@@ -127,6 +130,35 @@ case class CometScanRule(session: SparkSession)
     }
   }
 
+  /**
+   * Spark omits partition-column and nondeterministic predicates from a V1 
scan's data filters.
+   * Preserve the enclosing filter's make_date guard before core or contrib 
scans are converted.
+   */
+  private def tagScansWithMakeDateFilters(plan: SparkPlan): Unit = {
+    def tagScan(scan: FileSourceScanExec, hasMakeDateFilter: Boolean): Unit = {
+      // Parent context can change when Spark replans: do not retain a 
previous walk's guard.
+      scan.unsetTagValue(CometScanRule.DISABLE_ROW_FILTER_PUSHDOWN_TAG)
+      if (hasMakeDateFilter) {
+        scan.setTagValue(CometScanRule.DISABLE_ROW_FILTER_PUSHDOWN_TAG, ())
+      }
+    }
+
+    def visit(node: SparkPlan, hasMakeDateFilter: Boolean): Unit = node match {
+      case scan: FileSourceScanExec => tagScan(scan, hasMakeDateFilter)
+      case scan: CometScanExec => tagScan(scan.wrapped, hasMakeDateFilter)
+      case filter: FilterExec =>
+        visit(
+          filter.child,
+          hasMakeDateFilter || 
filter.condition.exists(_.isInstanceOf[MakeDate]))
+      case project: ProjectExec => visit(project.child, hasMakeDateFilter)
+      // Only the same filter/project pipeline shares predicate evaluation 
order. Joins and
+      // materializing operators must not propagate this guard into unrelated 
scans.
+      case _ => node.children.foreach(visit(_, hasMakeDateFilter = false))
+    }
+
+    visit(plan, hasMakeDateFilter = false)
+  }

Review Comment:
   This tag only feeds `disable_row_filter_pushdown`, which matters only with 
`spark.comet.parquet.rowFilterPushdown.enabled=true` (default `false`). The 
open findings on this head (dictionary pruning, NTZ, float operands, V1 
translation) all come from matching Spark's per-file pruning, which Spark 
builds from the footer schema. Would it be simpler to keep Spark's reader for a 
scan under a filter whose `make_date` can throw, reusing the bounded-year check 
so safe dates stay native? Then the proto field, the `init_datasource_exec` 
argument, the native checks and the Delta setters would not be needed. I expect 
that shape to be rare, but I have not measured it.



##########
spark/src/main/scala/org/apache/comet/rules/MakeDateEvaluation.scala:
##########
@@ -464,14 +479,19 @@ private[rules] object MakeDateEvaluation {
       (rebuilt, fallbackReason, dateDerivedOutputs)
     }
 
-    // Dataset.rdd adds an object-producing root, and its caller may read only 
a few objects.
-    // EliminateSerialization can leave a typed operator, filter, or project 
at that root.
-    def producesObjects(op: SparkPlan): Boolean = op match {
-      case _: ObjectProducerExec => true
-      case _: FilterExec | _: ProjectExec => producesObjects(op.children.head)
+    // QueryExecution.toRdd (also used by PySpark's DataFrame.rdd) exposes 
this plan's row
+    // iterator without adding Dataset.rdd's object-producing root. Its caller 
can stop early,
+    // even if explain() or another action has already prepared the physical 
plan.
+    // A one-row collect limit can retain its native input cap: producing its 
only row cannot
+    // skip another row inside that cap. Larger limits and OFFSET do not 
provide that guarantee.
+    def isSingleRowCollect(op: SparkPlan): Boolean = op match {
+      case limit: CollectLimitExec => limit.limit == 1 && limit.offset == 0
+      case native: CometExec => isSingleRowCollect(native.originalPlan)
+      case _: ColumnarToRowExec | _: CometColumnarToRowExec | _: 
CometNativeColumnarToRowExec =>
+        isSingleRowCollect(op.children.head)
       case _ => false
     }
-    val rootDemand = if (producesObjects(plan)) Some("code reading 
Dataset.rdd") else None
+    val rootDemand = if (isSingleRowCollect(plan)) None else 
Some(externalRowConsumer)

Review Comment:
   With this default every plan except a `LIMIT 1` collect starts with 
external-row demand, so a root projection with a `make_date` that can throw now 
runs in Spark in both ANSI modes. On `main` that applied only to the 
`Dataset.rdd` root. The fixtures show the reach: about 50 queries gained 
`REPARTITION(1)` to stay native, so plain `SELECT make_date(y, m, d) FROM t` is 
no longer tested as a native shape. It also drops #7283's native cap for `LIMIT 
n` with `n > 1`, including `df.show()` (`CollectLimitExec(21)`). Note that 
Spark's `CollectLimitExec.doExecute` evaluates `take(limit)` in every partition 
when the child has more than one, so only a single-partition child streams.
   
   Since the `DataFrame.rdd` gap already exists on `main` with ANSI on, could 
this routing be a separate follow-up to #7283 with a benchmark of the default 
path, plus one `expect_fallback` fixture for a plain root projection?



##########
native/core/src/parquet/parquet_exec.rs:
##########
@@ -351,6 +370,11 @@ fn calls_jvm(expr: &Arc<dyn PhysicalExpr>) -> bool {
         .unwrap_or(true)
 }
 
+fn contains_make_date(expr: &Arc<dyn PhysicalExpr>) -> bool {
+    expr.exists(|e| Ok(e.downcast_ref::<MakeDateExpr>().is_some()))
+        .unwrap_or(true)
+}

Review Comment:
   I could not find a path that sends a `MakeDate` predicate here. 
`buildNativeScanCommon`, the scalar-subquery path in `CometNativeScanExec` and 
both Delta paths run `filterForParquetPushdown` first, which returns `None` for 
any predicate containing `MakeDate`. If so, these checks and the ones at lines 
119 to 122 and 317 cannot match, and no native test exercises them. Could they 
go, so the decision lives only on the JVM side?



##########
spark/src/main/scala/org/apache/comet/rules/MakeDateEvaluation.scala:
##########
@@ -464,14 +479,19 @@ private[rules] object MakeDateEvaluation {
       (rebuilt, fallbackReason, dateDerivedOutputs)
     }
 
-    // Dataset.rdd adds an object-producing root, and its caller may read only 
a few objects.
-    // EliminateSerialization can leave a typed operator, filter, or project 
at that root.
-    def producesObjects(op: SparkPlan): Boolean = op match {
-      case _: ObjectProducerExec => true
-      case _: FilterExec | _: ProjectExec => producesObjects(op.children.head)
+    // QueryExecution.toRdd (also used by PySpark's DataFrame.rdd) exposes 
this plan's row
+    // iterator without adding Dataset.rdd's object-producing root. Its caller 
can stop early,
+    // even if explain() or another action has already prepared the physical 
plan.
+    // A one-row collect limit can retain its native input cap: producing its 
only row cannot
+    // skip another row inside that cap. Larger limits and OFFSET do not 
provide that guarantee.
+    def isSingleRowCollect(op: SparkPlan): Boolean = op match {
+      case limit: CollectLimitExec => limit.limit == 1 && limit.offset == 0
+      case native: CometExec => isSingleRowCollect(native.originalPlan)
+      case _: ColumnarToRowExec | _: CometColumnarToRowExec | _: 
CometNativeColumnarToRowExec =>

Review Comment:
   #7505 (approved) removes `CometNativeColumnarToRowExec`, including the 
existing match on it in this file. The two PRs merge without a textual 
conflict, but the merged tree still references the class on this line, so 
whichever lands second will not compile.



##########
spark/src/test/resources/sql-tests/expressions/datetime/make_date_ansi.sql:
##########
@@ -196,10 +199,49 @@ INSERT INTO make_date_shared VALUES (NULL, 0), (2024, 0)
 
 -- Spark computes the shared day expression outside the constructor's NULL 
checks.
 query expect_fallback(make_date requires Spark evaluation when a computed 
argument)
-SELECT make_date(y, 1, d + 1), d + 1 FROM make_date_shared
+SELECT /*+ REPARTITION(1) */ make_date(y, 1, d + 1), d + 1 FROM 
make_date_shared
 
 statement
 SET spark.sql.subexpressionElimination.enabled=false
 
 query expect_native(make_date)
-SELECT make_date(y, 1, d + 1), d + 1 FROM make_date_shared
+SELECT /*+ REPARTITION(1) */ make_date(y, 1, d + 1), d + 1 FROM 
make_date_shared
+
+-- The Date32 endpoints remain valid, and NULL arguments skip overflow 
validation.
+statement
+CREATE TABLE make_date_bounds(y INT, m INT, d INT) USING parquet
+
+statement
+INSERT INTO make_date_bounds VALUES
+  (5881580, 7, 11), (-5877641, 6, 23),
+  (NULL, 13, 32), (6000000, NULL, 1), (6000000, 1, NULL)
+
+query expect_native(make_date)
+SELECT /*+ REPARTITION(1) */ unix_date(make_date(y, m, d)) FROM 
make_date_bounds
+
+-- Valid calendar dates just outside Date32 must overflow, regardless of ANSI 
mode.
+statement
+CREATE TABLE make_date_overflow(y INT, m INT, d INT) USING parquet
+
+statement
+INSERT INTO make_date_overflow VALUES (5881580, 7, 12), (-5877641, 6, 22)
+
+query expect_error_class(ARITHMETIC_OVERFLOW)
+SELECT /*+ REPARTITION(1) */ make_date(y, m, d) FROM make_date_overflow WHERE 
y > 0
+
+query expect_error_class(ARITHMETIC_OVERFLOW)
+SELECT /*+ REPARTITION(1) */ make_date(y, m, d) FROM make_date_overflow WHERE 
y < 0
+
+-- An empty input must not evaluate overflowing scalar arguments.
+query expect_native(make_date)
+SELECT /*+ REPARTITION(1) */ make_date(6000000, 1, 1) FROM make_date_overflow 
WHERE y = 0

Review Comment:
   This block repeats the new block in `make_date.sql` and only the ANSI 
setting differs. Could it be one file with `-- ConfigMatrix: 
spark.sql.ansi.enabled=false,true`?



##########
spark/src/main/scala/org/apache/comet/serde/operator/CometNativeScan.scala:
##########
@@ -366,7 +428,7 @@ object CometNativeScan extends 
CometOperatorSerde[CometScanExec] with CometTypeS
     if (conf.getConf(SQLConf.PARQUET_FILTER_PUSHDOWN_ENABLED)) {
       commonBuilder.setHasDataFilters(dataFilters.nonEmpty)
       val filterProtos = new ListBuffer[Expr]()
-      for (filter <- dataFilters) {
+      for (filter <- dataFilters.flatMap(filterForParquetPushdown(_, conf))) {

Review Comment:
   `filterForParquetPushdown` now runs on the data filters of every native 
scan, so with a non-default `spark.sql.parquet.filterPushdown.date`, 
`.timestamp`, `.decimal` or `.string.predicate`, queries without `make_date` 
lose native pruning they have today. Honoring those settings may be right, but 
it is a scan change of its own. Could it move to a separate PR with a test that 
has no `make_date`, leaving only the whole-predicate `make_date` exclusion 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]


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

Reply via email to