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

peter-toth pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/master by this push:
     new 3ad5b64dbca1 [SPARK-57194][SQL] Add preOperatorOptimizationRules 
extension point to Optimizer
3ad5b64dbca1 is described below

commit 3ad5b64dbca11d6ba37bfdf6904e53ee2c00fd6a
Author: Shrirang Mhalgi <[email protected]>
AuthorDate: Wed Jun 10 11:57:36 2026 +0200

    [SPARK-57194][SQL] Add preOperatorOptimizationRules extension point to 
Optimizer
    
    ### What changes were proposed in this pull request?
    Add `preOperatorOptimizationRules` - a new optimizer extension point that 
runs in a `Once` batch between "Aggregate" and the main fixed-point "Operator 
Optimization" batch. Wired through 
`SparkSessionExtensions.injectPreOperatorOptimizationRule` and 
`BaseSessionStateBuilder`. The batch is a no-op when no rules are registered.
    
    ### Why are the changes needed?
    Custom rules injected via `injectOptimizerRule` run inside the fixed-point 
batch alongside built-in rules (FoldablePropagation, ConstantFolding, 
PushDownPredicates). A rule that needs to observe the original plan shape 
(e.g., cross-side join predicates before they are folded into single-side 
constants) is silently defeated when a built-in rule transforms the plan first 
within the same fixed-point iteration.
    
    None of the existing extension points cover this:
    
    - `extendedOperatorOptimizationRules` - same fixed-point batch
    - `postHocResolutionRules` - analyzer phase, too early
    - `earlyScanPushDownRules` - runs after optimization, scoped to scan 
pushdown
    - `preCBORules` - runs after operator optimization, too late
    
    ### Does this PR introduce _any_ user-facing change?
    Yes. New public API will be available: 
`SparkSessionExtensions.injectPreOperatorOptimizationRule(builder: 
RuleBuilder)`.
    
    ### How was this patch tested?
    - New test in `SparkSessionExtensionSuite` verifying the injected rule is 
wired into the `preOperatorOptimizationRules` accessor.
    - All existing SparkSessionExtensionSuite tests pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Yes.
    
    Closes #56277 from shrirangmhalgi/SPARK-57194-early-optimizer-rules.
    
    Authored-by: Shrirang Mhalgi <[email protected]>
    Signed-off-by: Peter Toth <[email protected]>
---
 .../apache/spark/sql/catalyst/optimizer/Optimizer.scala | 12 ++++++++++++
 .../org/apache/spark/sql/SparkSessionExtensions.scala   | 17 +++++++++++++++++
 .../spark/sql/internal/BaseSessionStateBuilder.scala    | 13 +++++++++++++
 .../apache/spark/sql/SparkSessionExtensionSuite.scala   |  6 ++++++
 4 files changed, 48 insertions(+)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala
index a1fe4cd8674b..42914e98aa6d 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala
@@ -228,6 +228,10 @@ abstract class Optimizer(catalogManager: CatalogManager)
     Batch("Aggregate", fixedPoint,
       RemoveLiteralFromGroupExpressions,
       RemoveRepetitionFromGroupExpressions),
+    // Injected rules run once here, before the operator-optimization fixed 
point, so they
+    // can observe the plan before FoldablePropagation/ConstantFolding rewrite 
it.
+    Batch("Pre Operator Optimization", Once,
+      preOperatorOptimizationRules: _*),
     operatorOptimizationBatch,
     Batch("Clean Up Temporary CTE Info", Once, CleanUpTempCTEInfo),
     // This batch rewrites plans after the operator optimization and
@@ -507,6 +511,14 @@ abstract class Optimizer(catalogManager: CatalogManager)
    */
   def extendedOperatorOptimizationRules: Seq[Rule[LogicalPlan]] = Nil
 
+  /**
+   * Override to provide additional rules that run in a single pass before the 
main
+   * operator optimization fixed-point batch. Use this for rules that need to 
observe the plan
+   * as it enters the operator-optimization fixed point (e.g., before 
FoldablePropagation or
+   * ConstantFolding rewrite predicates).
+   */
+  def preOperatorOptimizationRules: Seq[Rule[LogicalPlan]] = Nil
+
   /**
    * Override to provide additional rules for early projection and filter 
pushdown to scans.
    */
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/SparkSessionExtensions.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/SparkSessionExtensions.scala
index 56bff0acc352..5de29732834a 100644
--- a/sql/core/src/main/scala/org/apache/spark/sql/SparkSessionExtensions.scala
+++ b/sql/core/src/main/scala/org/apache/spark/sql/SparkSessionExtensions.scala
@@ -309,6 +309,23 @@ class SparkSessionExtensions {
     optimizerRules += builder
   }
 
+  private[this] val preOperatorOptimizationRuleBuilders = 
mutable.Buffer.empty[RuleBuilder]
+
+  private[sql] def buildPreOperatorOptimizationRules(
+      session: SparkSession): Seq[Rule[LogicalPlan]] = {
+    preOperatorOptimizationRuleBuilders.map(_.apply(session)).toSeq
+  }
+
+  /**
+   * Inject an optimizer `Rule` builder into the [[SparkSession]]. The 
injected rules will be
+   * executed in a single pass (Once) before the main operator optimization 
fixed-point batch.
+   * Use this for rules that need to observe the plan as it enters the 
operator-optimization
+   * fixed point, before built-in rules like FoldablePropagation or 
ConstantFolding transform it.
+   */
+  def injectPreOperatorOptimizationRule(builder: RuleBuilder): Unit = {
+    preOperatorOptimizationRuleBuilders += builder
+  }
+
   private[this] val preCBORules = mutable.Buffer.empty[RuleBuilder]
 
   private[sql] def buildPreCBORules(session: SparkSession): 
Seq[Rule[LogicalPlan]] = {
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/internal/BaseSessionStateBuilder.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/internal/BaseSessionStateBuilder.scala
index 4838e6d60f72..52c6821d0001 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/internal/BaseSessionStateBuilder.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/internal/BaseSessionStateBuilder.scala
@@ -332,6 +332,9 @@ abstract class BaseSessionStateBuilder(
 
       override def extendedOperatorOptimizationRules: Seq[Rule[LogicalPlan]] =
         super.extendedOperatorOptimizationRules ++ 
customOperatorOptimizationRules
+
+      override def preOperatorOptimizationRules: Seq[Rule[LogicalPlan]] =
+        super.preOperatorOptimizationRules ++ 
customPreOperatorOptimizationRules
     }
   }
 
@@ -345,6 +348,16 @@ abstract class BaseSessionStateBuilder(
     extensions.buildOptimizerRules(session)
   }
 
+  /**
+   * Custom rules that run in a single pass before the main operator 
optimization batch.
+   * Prefer overriding this instead of creating your own Optimizer.
+   *
+   * Note that this may NOT depend on the `optimizer` function.
+   */
+  protected def customPreOperatorOptimizationRules: Seq[Rule[LogicalPlan]] = {
+    extensions.buildPreOperatorOptimizationRules(session)
+  }
+
   /**
    * Custom early scan push down rules to add to the Optimizer. Prefer 
overriding this instead
    * of creating your own Optimizer.
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/SparkSessionExtensionSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/SparkSessionExtensionSuite.scala
index 89dd1a629af1..d9b29b757b76 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/SparkSessionExtensionSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/SparkSessionExtensionSuite.scala
@@ -132,6 +132,12 @@ class SparkSessionExtensionSuite extends PlanTest with 
AdaptiveSparkPlanHelper {
     }
   }
 
+  test("SPARK-57194: inject pre operator optimization rule") {
+    withSession(Seq(_.injectPreOperatorOptimizationRule(MyRule))) { session =>
+      
assert(session.sessionState.optimizer.preOperatorOptimizationRules.contains(MyRule(session)))
+    }
+  }
+
   test("inject spark planner strategy") {
     withSession(Seq(_.injectPlannerStrategy(MySparkStrategy))) { session =>
       
assert(session.sessionState.planner.strategies.contains(MySparkStrategy(session)))


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

Reply via email to