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

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


The following commit(s) were added to refs/heads/branch-4.0 by this push:
     new 9dede1fa81fb [SPARK-57505][SQL] Do not wrap window functions in an 
`AggregateExpression` when converting an expression-backed `Column`
9dede1fa81fb is described below

commit 9dede1fa81fb202b95764483dfe2dd118330ce87
Author: Peter Toth <[email protected]>
AuthorDate: Thu Jun 18 13:07:16 2026 +0200

    [SPARK-57505][SQL] Do not wrap window functions in an `AggregateExpression` 
when converting an expression-backed `Column`
    
    ### What changes were proposed in this pull request?
    
    `ColumnNodeToExpressionConverter` (sql/core 
.../classic/columnNodeSupport.scala) converts an expression-backed `Column` (an 
`ExpressionColumnNode`) into a Catalyst `Expression` and, if the wrapped 
expression is an `AggregateFunction`, wraps it in an `AggregateExpression` via 
`toAggregateExpression()`. Because `AggregateWindowFunction extends 
DeclarativeAggregate (which extends AggregateFunction) with WindowFunction`, a 
window function also matched this branch and got wrapped, producin [...]
    
    ### Why are the changes needed?
    
    Wrapping a window function in an `AggregateExpression` makes analysis fail: 
`CheckAnalysis` sees an `AggregateExpression` whose child is a `WindowFunction` 
but which is not itself a `WindowExpression`, and throws 
`WINDOW_FUNCTION_WITHOUT_OVER_CLAUSE`. As a result, a user-defined 
`AggregateWindowFunction` (or any built-in window function expression) wrapped 
into a `Column` through the `ClassicConversions` / `ColumnConversions` 
DeveloperApi and used with `over(...)` could not be analyze [...]
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Before this change, wrapping a window function expression into a 
`Column` and calling `.over(window)` failed analysis with 
`WINDOW_FUNCTION_WITHOUT_OVER_CLAUSE`; now it analyzes and executes correctly, 
matching the behavior of the equivalent by-name window function. This is a fix 
within the unreleased master branch relative to the existing Column/Catalyst 
conversion behavior.
    
    ### How was this patch tested?
    
    Added two regression tests to `DataFrameWindowFunctionsSuite`: one wraps 
the built-in `RowNumber()` expression into a `Column` and checks it matches 
by-name `row_number()`, and one defines a minimal custom 
`AggregateWindowFunction` (`NonNullRunningCount`) and checks it produces 
correct results when used with `over(...)`.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 4.8
    
    Closes #56570 from 
peter-toth/SPARK-57505-fix-custom-aggregatewindowfunction-analysis.
    
    Authored-by: Peter Toth <[email protected]>
    Signed-off-by: Peter Toth <[email protected]>
    (cherry picked from commit 201da65b7add4be81bb466183a85a88e61edaf30)
    Signed-off-by: Peter Toth <[email protected]>
---
 .../spark/sql/classic/columnNodeSupport.scala      |  9 ++++-
 .../spark/sql/DataFrameWindowFunctionsSuite.scala  | 47 +++++++++++++++++++++-
 2 files changed, 53 insertions(+), 3 deletions(-)

diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/classic/columnNodeSupport.scala 
b/sql/core/src/main/scala/org/apache/spark/sql/classic/columnNodeSupport.scala
index 5766535ac5da..4fda00b1c79a 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/classic/columnNodeSupport.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/classic/columnNodeSupport.scala
@@ -22,7 +22,7 @@ import org.apache.spark.SparkException
 import org.apache.spark.sql.Column
 import org.apache.spark.sql.catalyst.{analysis, expressions, 
CatalystTypeConverters}
 import org.apache.spark.sql.catalyst.analysis.{MultiAlias, UnresolvedAlias}
-import org.apache.spark.sql.catalyst.expressions.{AttributeReference, 
Expression, Generator, NamedExpression, Unevaluable}
+import org.apache.spark.sql.catalyst.expressions.{AttributeReference, 
Expression, Generator, NamedExpression, Unevaluable, WindowFunction}
 import 
org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, 
AggregateFunction}
 import org.apache.spark.sql.catalyst.parser.{ParserInterface, ParserUtils}
 import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan
@@ -185,7 +185,12 @@ private[sql] trait ColumnNodeToExpressionConverter extends 
(ColumnNode => Expres
             case ColumnNodeExpression(node) => apply(node)
           }
           transformed match {
-            case f: AggregateFunction => f.toAggregateExpression()
+            // A window function (e.g. an AggregateWindowFunction) is also an 
AggregateFunction, but
+            // it must not be wrapped in an AggregateExpression: it is used 
directly as the child of
+            // a WindowExpression. Wrapping it would later fail analysis with
+            // WINDOW_FUNCTION_WITHOUT_OVER_CLAUSE.
+            case f: AggregateFunction if !f.isInstanceOf[WindowFunction] =>
+              f.toAggregateExpression()
             case _ => transformed
           }
 
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameWindowFunctionsSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameWindowFunctionsSuite.scala
index 01e72daead44..552e2b2e2749 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameWindowFunctionsSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameWindowFunctionsSuite.scala
@@ -20,10 +20,11 @@ package org.apache.spark.sql
 import org.scalatest.matchers.must.Matchers.the
 
 import org.apache.spark.TestUtils.{assertNotSpilled, assertSpilled}
-import org.apache.spark.sql.catalyst.expressions.{AttributeReference, 
Expression, Lag, Literal, NonFoldableLiteral}
+import org.apache.spark.sql.catalyst.expressions.{Add, 
AggregateWindowFunction, AttributeReference, Expression, If, IsNotNull, Lag, 
Literal, NonFoldableLiteral, RowNumber}
 import org.apache.spark.sql.catalyst.optimizer.TransposeWindow
 import org.apache.spark.sql.catalyst.plans.logical.{Window => LogicalWindow}
 import org.apache.spark.sql.catalyst.plans.physical.HashPartitioning
+import org.apache.spark.sql.catalyst.trees.UnaryLike
 import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
 import org.apache.spark.sql.execution.exchange.{ENSURE_REQUIREMENTS, Exchange, 
ShuffleExchangeExec}
 import org.apache.spark.sql.execution.window.WindowExec
@@ -879,6 +880,28 @@ class DataFrameWindowFunctionsSuite extends QueryTest
     )
   }
 
+  test("SPARK-57505: a window function expression wrapped into a Column works 
with over()") {
+    val df = Seq((1, "a"), (2, "a"), (3, "b")).toDF("value", "key")
+    val window = Window.partitionBy($"key").orderBy($"value")
+    // Wrapping a catalyst window function expression directly with 
Column(expr) used to box the
+    // AggregateWindowFunction (RowNumber is one) in an AggregateExpression, 
which then failed
+    // analysis with WINDOW_FUNCTION_WITHOUT_OVER_CLAUSE. It must now behave 
like by-name
+    // row_number().
+    checkAnswer(
+      df.select($"value", Column(RowNumber()).over(window).as("rn")),
+      Seq(Row(1, 1), Row(2, 2), Row(3, 1)))
+  }
+
+  test("SPARK-57505: a custom AggregateWindowFunction wrapped into a Column 
works with over()") {
+    val df = Seq((1, "a"), (2, "a"), (3, "b")).toDF("value", "key")
+    val window = Window.partitionBy($"key").orderBy($"value")
+    // Mirrors plugging in a user-defined AggregateWindowFunction through the 
Column API:
+    //   Column(MyWindowFunction(inputColumn.expr)).over(window)
+    checkAnswer(
+      df.select($"value", 
Column(NonNullRunningCount($"value".expr)).over(window).as("cnt")),
+      Seq(Row(1, 1), Row(2, 2), Row(3, 1)))
+  }
+
   test("SPARK-12989 ExtractWindowExpressions treats alias as regular 
attribute") {
     val src = Seq((0, 3, 5)).toDF("a", "b", "c")
       .withColumn("Data", struct("a", "b"))
@@ -1620,3 +1643,25 @@ class DataFrameWindowFunctionsSuite extends QueryTest
     }
   }
 }
+
+/**
+ * A minimal user-defined window function, it counts the non-null values of 
`child` from the start
+ * of the window frame up to and including the current row.
+ */
+case class NonNullRunningCount(child: Expression)
+  extends AggregateWindowFunction with UnaryLike[Expression] {
+
+  private lazy val count = AttributeReference("count", IntegerType, nullable = 
false)()
+
+  override lazy val aggBufferAttributes: Seq[AttributeReference] = count :: Nil
+  override lazy val initialValues: Seq[Expression] = Literal(0) :: Nil
+  override lazy val updateExpressions: Seq[Expression] =
+    If(IsNotNull(child), Add(count, Literal(1)), count) :: Nil
+  override lazy val evaluateExpression: Expression = count
+
+  override def nullable: Boolean = false
+  override def prettyName: String = "non_null_running_count"
+
+  override protected def withNewChildInternal(newChild: Expression): 
NonNullRunningCount =
+    copy(child = newChild)
+}


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

Reply via email to