This is an automated email from the ASF dual-hosted git repository.

szehon-ho pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new 450575d41f7f [SPARK-58112][SQL] Skip pushdown for nondeterministic 
Catalyst filters
450575d41f7f is described below

commit 450575d41f7fcae1dc1b4fa3731bc3224a151ae0
Author: Szehon Ho <[email protected]>
AuthorDate: Tue Jul 14 14:01:08 2026 -0700

    [SPARK-58112][SQL] Skip pushdown for nondeterministic Catalyst filters
    
    ### What changes were proposed in this pull request?
    
    This PR updates Catalyst filter pushdown for 
`SupportsPushDownCatalystFilters` so Spark only passes deterministic filters to 
the connector pushdown API. Nondeterministic filters are kept as post-scan 
filters and evaluated by Spark after the scan.
    
    This also adds a regression test with a test data source implementing 
`SupportsPushDownCatalystFilters`; the source fails if a nondeterministic 
predicate reaches `pushFilters`.
    
    ### Why are the changes needed?
    
    Pushing nondeterministic filters into a data source can change query 
semantics because the filter may be evaluated at a different point or a 
different number of times than Spark expects. The existing V2 file scan path 
already avoids pushing nondeterministic filters; this makes the Catalyst filter 
pushdown utility apply the same guard before invoking 
`SupportsPushDownCatalystFilters`.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Data sources using `SupportsPushDownCatalystFilters` will no longer 
receive nondeterministic filters for pushdown. Those filters remain in Spark as 
post-scan filters.
    
    ### How was this patch tested?
    
    Added a regression test in `DataSourceV2Suite`: `catalyst filter pushdown 
skips non-deterministic filters`.
    
    Local checks run:
    
    ```bash
    git diff --check
    grep -rn -P "[^\x00-\x7F]" 
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
 sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2Suite.scala
    JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 
MAVEN_MIRROR_URL=https://maven-proxy.cloud.databricks.com build/sbt 
'sql/testOnly *DataSourceV2Suite -- -z "catalyst filter pushdown skips 
non-deterministic filters"'\n```\n\nThe focused SBT test passed locally: 1 test 
run, 1 succeeded, 0 failed.\n\n### Was this patch authored or co-authored using 
generative AI tooling?\n\nGenerated-by: Codex (GPT-5)\n
    
    Closes #57235 from szehon-ho/skip-nondeterministic-catalyst-filter-pushdown.
    
    Authored-by: Szehon Ho <[email protected]>
    Signed-off-by: Szehon Ho <[email protected]>
    (cherry picked from commit 233b1588b475e9c64dc10d8088baf19c1027d21c)
    Signed-off-by: Szehon Ho <[email protected]>
---
 .../execution/datasources/v2/PushDownUtils.scala   |  3 +-
 .../spark/sql/connector/DataSourceV2Suite.scala    | 56 +++++++++++++++++++++-
 2 files changed, 56 insertions(+), 3 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
index 7df6ade32c48..6685b28c7dc8 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/PushDownUtils.scala
@@ -134,7 +134,8 @@ object PushDownUtils extends Logging {
           ExpressionSet(untranslatableExprs))
         (Right(r.pushedPredicates.toImmutableArraySeq), orderedPostScanFilters)
       case r: SupportsPushDownCatalystFilters =>
-        val postScanFilters = r.pushFilters(filters)
+        val (deterministicFilters, nonDeterministicFilters) = 
filters.partition(_.deterministic)
+        val postScanFilters = r.pushFilters(deterministicFilters) ++ 
nonDeterministicFilters
         (Right(r.pushedFilters.toImmutableArraySeq), postScanFilters)
       case _ => (Left(Nil), filters)
     }
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2Suite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2Suite.scala
index 5ae23bc3338c..c6f3c1c3886c 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2Suite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/connector/DataSourceV2Suite.scala
@@ -28,8 +28,10 @@ import test.org.apache.spark.sql.connector._
 import org.apache.spark.SparkUnsupportedOperationException
 import org.apache.spark.sql.{AnalysisException, DataFrame, Row}
 import org.apache.spark.sql.catalyst.InternalRow
-import org.apache.spark.sql.catalyst.expressions.{AttributeReference, 
GreaterThan => CatalystGreaterThan, Literal => CatalystLiteral, ScalarSubquery}
-import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, Project}
+import org.apache.spark.sql.catalyst.expressions.{
+  AttributeReference, Expression => CatalystExpression, GreaterThan => 
CatalystGreaterThan,
+  Literal => CatalystLiteral, ScalarSubquery}
+import org.apache.spark.sql.catalyst.plans.logical.{Aggregate, Filter => 
LogicalFilter, Project}
 import org.apache.spark.sql.connector.catalog.{PartitionInternalRow, 
SupportsRead, Table, TableCapability, TableProvider}
 import org.apache.spark.sql.connector.catalog.TableCapability._
 import org.apache.spark.sql.connector.expressions.{Expression, FieldReference, 
Literal, NamedReference, NullOrdering, SortDirection, SortOrder, Transform}
@@ -46,6 +48,7 @@ import 
org.apache.spark.sql.execution.vectorized.OnHeapColumnVector
 import org.apache.spark.sql.expressions.Window
 import org.apache.spark.sql.functions._
 import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.internal.connector.SupportsPushDownCatalystFilters
 import org.apache.spark.sql.sources.{Filter, GreaterThan}
 import org.apache.spark.sql.test.SharedSparkSession
 import org.apache.spark.sql.types.{IntegerType, StructField, StructType}
@@ -1351,6 +1354,28 @@ class DataSourceV2Suite extends SharedSparkSession with 
AdaptiveSparkPlanHelper
       "pushedFilters should contain the pushed filter on column i")
   }
 
+  test("catalyst filter pushdown skips non-deterministic filters") {
+    val df = 
spark.read.format(classOf[CatalystFilterDataSourceV2].getName).load()
+    val q = df.filter($"i" > 3 && rand() > 0.5)
+
+    val scanRelation = getScanRelation(q)
+    assert(scanRelation.pushedFilters.nonEmpty,
+      "pushedFilters should contain the deterministic pushed filter")
+    assert(scanRelation.pushedFilters.forall(_.deterministic),
+      "pushedFilters should not contain non-deterministic filters")
+    val referencedCols = 
scanRelation.pushedFilters.flatMap(_.references.map(_.name)).toSet
+    assert(referencedCols.contains("i"),
+      "pushedFilters should contain the pushed filter on column i")
+
+    // The non-deterministic filter is not pushed, so it must be retained as a 
post-scan
+    // Filter above the scan to still be evaluated.
+    val postScanConditions = q.queryExecution.optimizedPlan.collect {
+      case f: LogicalFilter => f.condition
+    }
+    assert(postScanConditions.exists(cond => cond.exists(!_.deterministic)),
+      "non-deterministic filter should be retained as a post-scan Filter")
+  }
+
   test("pushedFilters drops filters referencing pruned columns") {
     // Disable constraint propagation so IsNotNull(i) is not added (it would 
keep
     // column i in the scan output). This simulates a connector that pushes 
IsNotNull.
@@ -1503,6 +1528,33 @@ class AdvancedDataSourceV2 extends TestingV2Source {
   }
 }
 
+class CatalystFilterDataSourceV2 extends TestingV2Source {
+
+  override def getTable(options: CaseInsensitiveStringMap): Table = new 
SimpleBatchTable {
+    override def newScanBuilder(options: CaseInsensitiveStringMap): 
ScanBuilder = {
+      new CatalystFilterScanBuilder()
+    }
+  }
+}
+
+class CatalystFilterScanBuilder extends SimpleScanBuilder
+  with SupportsPushDownCatalystFilters {
+
+  override def pushFilters(filters: Seq[CatalystExpression]): 
Seq[CatalystExpression] = {
+    if (filters.exists(!_.deterministic)) {
+      throw new IllegalArgumentException(
+        s"Non-deterministic filters should not be pushed: 
${filters.mkString(", ")}")
+    }
+    Nil
+  }
+
+  override def pushedFilters: Array[Predicate] = Array.empty
+
+  override def planInputPartitions(): Array[InputPartition] = {
+    throw new IllegalArgumentException("planInputPartitions must not be 
called")
+  }
+}
+
 class AdvancedScanBuilder extends ScanBuilder
   with Scan with SupportsPushDownFilters with SupportsPushDownRequiredColumns {
 


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

Reply via email to